Title: Coding
Tags: leetcode

Q: Problem: Compress a string such that 'AAABCCDDDD' becomes 'A3BC2D4'. Only compress the string if it saves space.
A: Constraints
- Can we assume the string is ASCII?
  - Yes
  - Note: Unicode strings could require special handling depending on your language
- Is this case sensitive?
  - Yes
- Can we use additional data structures?
  - Yes
- Can we assume this fits in memory?
  - Yes
Test Cases
- None -> None
- '' -> ''
- 'AABBCC' -> 'AABBCC'
- 'AAABCCDDDD' -> 'A3BC2D4'
Algorithm
- For each char in string
  - If char is the same as last_char, increment count
  - Else
    - Append last_char and count to compressed_string
    - last_char = char
    - count = 1
- Append last_char and count to compressed_string
- If the compressed string size is < string size
  - Return compressed string
- Else
  - Return string
Complexity:
- Time: O(n)
- Space: O(n)
Complexity Note:
- Although strings are immutable in Python, appending to strings is optimized in CPython so that it now runs in O(n) and extends the string in-place. Refer to this Stack Overflow post.
Code
class CompressString(object):
​
    def compress(self, string):
        if string is None or not string:
            return string
        result = ''
        prev_char = string[0]
        count = 0
        for char in string:
            if char == prev_char:
                count += 1
            else:
                result += self._calc_partial_result(prev_char, count)
                prev_char = char
                count = 1
        result += self._calc_partial_result(prev_char, count)
        return result if len(result) < len(string) else string
​
    def _calc_partial_result(self, prev_char, count):
        return prev_char + (str(count) if count > 1 else '')
Unit Test
%%writefile test_compress.py
from nose.tools import assert_equal
​
​
class TestCompress(object):
​
    def test_compress(self, func):
        assert_equal(func(None), None)
        assert_equal(func(''), '')
        assert_equal(func('AABBCC'), 'AABBCC')
        assert_equal(func('AAABCCDDDDE'), 'A3BC2D4E')
        assert_equal(func('BAAACCDDDD'), 'BA3C2D4')
        assert_equal(func('AAABAACCDDDD'), 'A3BA2C2D4')
        print('Success: test_compress')
​
​
def main():
    test = TestCompress()
    compress_string = CompressString()
    test.test_compress(compress_string.compress)
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement a hash table with set, get, and remove methods.
A: Constraints
- For simplicity, are the keys integers only?
  - Yes
- For collision resolution, can we use chaining?
  - Yes
- Do we have to worry about load factors?
  - No
- Do we have to validate inputs?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- get no matching key -> KeyError exception
- get matching key -> value
- set no matchin gkey -> new key, value
- set matching key -> update value
- remove no matching key -> KeyError exception
- remove matching key -> remove key, value
Algorithm
Hash Function
- Return key % table size
Complexity:
- Time: O(1)
- Space: O(1)
Set
- Get hash index for lookup
- If key exists, replace
- Else, add
Complexity:
- Time: O(1) average and best, O(n) worst
- Space: O(1) space for newly added element
Get
- Get hash index for lookup
- If key exists, return value
- Else, raise KeyError
Complexity:
- Time: O(1) average and best, O(n) worst
- Space: O(1)
Remove
- Get hash index for lookup
- If key exists, delete the item
- Else, raise KeyError
Complexity:
- Time: O(1) average and best, O(n) worst
- Space: O(1)
Code
class Item(object):
​
    def __init__(self, key, value):
        self.key = key
        self.value = value
​
​
class HashTable(object):
​
    def __init__(self, size):
        self.size = size
        self.table = [[] for _ in range(self.size)]
​
    def _hash_function(self, key):
        return key % self.size
​
    def set(self, key, value):
        hash_index = self._hash_function(key)
        for item in self.table[hash_index]:
            if item.key == key:
                item.value = value
                return
        self.table[hash_index].append(Item(key, value))
​
    def get(self, key):
        hash_index = self._hash_function(key)
        for item in self.table[hash_index]:
            if item.key == key:
                return item.value
        raise KeyError('Key not found')
​
    def remove(self, key):
        hash_index = self._hash_function(key)
        for index, item in enumerate(self.table[hash_index]):
            if item.key == key:
                del self.table[hash_index][index]
                return
        raise KeyError('Key not found')
Unit Test
%%writefile test_hash_map.py
from nose.tools import assert_equal, assert_raises
​
​
class TestHashMap(object):
​
    # TODO: It would be better if we had unit tests for each
    # method in addition to the following end-to-end test
    def test_end_to_end(self):
        hash_table = HashTable(10)
​
        print("Test: get on an empty hash table index")
        assert_raises(KeyError, hash_table.get, 0)
​
        print("Test: set on an empty hash table index")
        hash_table.set(0, 'foo')
        assert_equal(hash_table.get(0), 'foo')
        hash_table.set(1, 'bar')
        assert_equal(hash_table.get(1), 'bar')
​
        print("Test: set on a non empty hash table index")
        hash_table.set(10, 'foo2')
        assert_equal(hash_table.get(0), 'foo')
        assert_equal(hash_table.get(10), 'foo2')
​
        print("Test: set on a key that already exists")
        hash_table.set(10, 'foo3')
        assert_equal(hash_table.get(0), 'foo')
        assert_equal(hash_table.get(10), 'foo3')
​
        print("Test: remove on a key that already exists")
        hash_table.remove(10)
        assert_equal(hash_table.get(0), 'foo')
        assert_raises(KeyError, hash_table.get, 10)
​
        print("Test: remove on a key that doesn't exist")
        assert_raises(KeyError, hash_table.remove, -1)
​
        print('Success: test_end_to_end')
​
​
def main():
    test = TestHashMap()
    test.test_end_to_end()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Determine if a string is a permutation of another string.
A: Constraints
- Can we assume the string is ASCII?
  - Yes
  - Note: Unicode strings could require special handling depending on your language
- Is whitespace important?
  - Yes
- Is this case sensitive? 'Nib', 'bin' is not a match?
  - Yes
- Can we use additional data structures?
  - Yes
- Can we assume this fits in memory?
  - Yes
Test Cases
- One or more None inputs -> False
- One or more empty strings -> False
- 'Nib', 'bin' -> False
- 'act', 'cat' -> True
- 'a ct', 'ca t' -> True
Algorithm: Compare Sorted Strings
Permutations contain the same strings but in different orders. This approach could be slow for large strings due to sorting.
- Sort both strings
- If both sorted strings are equal
  - return True
- Else
  - return False
Complexity:
- Time: O(n log n) from the sort, in general
- Space: O(n)
Code: Compare Sorted Strings
class Permutations(object):
​
    def is_permutation(self, str1, str2):
        if str1 is None or str2 is None:
            return False
        return sorted(str1) == sorted(str2)
Algorithm: Hash Map Lookup
We'll keep a hash map (dict) to keep track of characters we encounter.
Steps:
- Scan each character
- For each character in each string:
  - If the character does not exist in a hash map, add the character to a hash map
  - Else, increment the character's count
- If the hash maps for each string are equal
  - Return True
- Else
  - Return False
Notes:
- Since the characters are in ASCII, we could potentially use an array of size 128 (or 256 for extended ASCII), where each array index is equivalent to an ASCII value
- Instead of using two hash maps, you could use one hash map and increment character values based on the first string and decrement based on the second string
- You can short circuit if the lengths of each string are not equal, although len() in Python is generally O(1) unlike other languages like C where getting the length of a string is O(n)
Complexity:
- Time: O(n)
- Space: O(n)
Code: Hash Map Lookup
from collections import defaultdict
​
​
class PermutationsAlt(object):
​
    def is_permutation(self, str1, str2):
        if str1 is None or str2 is None:
            return False
        if len(str1) != len(str2):
            return False
        unique_counts1 = defaultdict(int)
        unique_counts2 = defaultdict(int)
        for char in str1:
            unique_counts1[char] += 1
        for char in str2:
            unique_counts2[char] += 1
        return unique_counts1 == unique_counts2
Unit Test
%%writefile test_permutation_solution.py
from nose.tools import assert_equal
​
​
class TestPermutation(object):
​
    def test_permutation(self, func):
        assert_equal(func(None, 'foo'), False)
        assert_equal(func('', 'foo'), False)
        assert_equal(func('Nib', 'bin'), False)
        assert_equal(func('act', 'cat'), True)
        assert_equal(func('a ct', 'ca t'), True)
        print('Success: test_permutation')
​
​
def main():
    test = TestPermutation()
    permutations = Permutations()
    test.test_permutation(permutations.is_permutation)
    try:
        permutations_alt = PermutationsAlt()
        test.test_permutation(permutations_alt.is_permutation)
    except NameError:
        # Alternate solutions are only defined
        # in the solutions file
        pass
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement a priority queue backed by an array.
A: Constraints
- Do we expect the methods to be insert, extract_min, and decrease_key?
  - Yes
- Can we assume there aren't any duplicate keys?
  - Yes
- Do we need to validate inputs?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
insert
- insert general case -> inserted node
extract_min
- extract_min from an empty list -> None
- extract_min general case -> min node
decrease_key
- decrease_key an invalid key -> None
- decrease_key general case -> updated node
Algorithm
insert
- Append to the internal array.
Complexity:
- Time: O(1)
- Space: O(1)
extract_min
- Loop through each item in the internal array
  - Update the min value as needed
- Remove the min element from the array and return it
Complexity:
- Time: O(n)
- Space: O(1)
decrease_key
- Loop through each item in the internal array to find the matching input
  - Update the matching element's key
Complexity:
- Time: O(n)
- Space: O(1)
Code
%%writefile priority_queue.py
import sys
​
​
class PriorityQueueNode(object):
​
    def __init__(self, obj, key):
        self.obj = obj
        self.key = key
​
    def __repr__(self):
        return str(self.obj) + ': ' + str(self.key)
​
​
class PriorityQueue(object):
​
    def __init__(self):
        self.array = []
​
    def __len__(self):
        return len(self.array)
​
    def insert(self, node):
        self.array.append(node)
        return self.array[-1]
​
    def extract_min(self):
        if not self.array:
            return None
        minimum = sys.maxsize
        for index, node in enumerate(self.array):
            if node.key < minimum:
                minimum = node.key
                minimum_index = index
        return self.array.pop(minimum_index)
​
    def decrease_key(self, obj, new_key):
        for node in self.array:
            if node.obj is obj:
                node.key = new_key
                return node
        return None
Unit Test
%%writefile test_priority_queue.py
from nose.tools import assert_equal
​
​
class TestPriorityQueue(object):
​
    def test_priority_queue(self):
        priority_queue = PriorityQueue()
        assert_equal(priority_queue.extract_min(), None)
        priority_queue.insert(PriorityQueueNode('a', 20))
        priority_queue.insert(PriorityQueueNode('b', 5))
        priority_queue.insert(PriorityQueueNode('c', 15))
        priority_queue.insert(PriorityQueueNode('d', 22))
        priority_queue.insert(PriorityQueueNode('e', 40))
        priority_queue.insert(PriorityQueueNode('f', 3))
        priority_queue.decrease_key('f', 2)
        priority_queue.decrease_key('a', 19)
        mins = []
        while priority_queue.array:
            mins.append(priority_queue.extract_min().key)
        assert_equal(mins, [2, 5, 15, 19, 22, 40])
        print('Success: test_min_heap')
​
​
def main():
    test = TestPriorityQueue()
    test.test_priority_queue()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement a function to reverse a string (a list of characters), in-place.
A: Constraints
- Can we assume the string is ASCII?
  - Yes
  - Note: Unicode strings could require special handling depending on your language
- Since we need to do this in-place, it seems we cannot use the slice operator or the reversed function?
  - Correct
- Since Python string are immutable, can we use a list of characters instead?
  - Yes
Test Cases
- None -> None
- [''] -> ['']
- ['f', 'o', 'o', ' ', 'b', 'a', 'r'] -> ['r', 'a', 'b', ' ', 'o', 'o', 'f']
Algorithm
Since Python strings are immutable, we'll use a list of chars instead to exercise in-place string manipulation as you would get with a C string.
- Iterate len(string)/2 times, starting with i = 0:
  - Swap i and len(string) - 1 - i
  - Increment i
Complexity:
- Time: O(n)
- Space: O(1)
Note:
- You could use a byte array instead of a list to do in-place string operations
Code
from __future__ import division
​
​
class ReverseString(object):
​
    def reverse(self, chars):
        if chars is None or not chars:
            return chars
        size = len(chars)
        for i in range(size // 2):
            chars[i], chars[size - 1 - i] = \
                chars[size - 1 - i], chars[i]
        return chars
Pythonic-Code
This question has an artificial constraint that prevented the use of the slice operator and the reversed method. For completeness, the solutions for these are provided below. Note these solutions are not in-place.
class ReverseStringAlt(object):
​
    def reverse_string_alt(string):
        if string is None or not string:
            return string
        return string[::-1]
​
    def reverse_string_alt2(string):
        if string is None or not string:
            return string
        return ''.join(reversed(string))
Unit Test
%%writefile test_reverse_string.py
from nose.tools import assert_equal
​
​
class TestReverse(object):
​
    def test_reverse(self, func):
        assert_equal(func(None), None)
        assert_equal(func(['']), [''])
        assert_equal(func(
            ['f', 'o', 'o', ' ', 'b', 'a', 'r']),
            ['r', 'a', 'b', ' ', 'o', 'o', 'f'])
        print('Success: test_reverse')
​
    def test_reverse_inplace(self, func):
        target_list = ['f', 'o', 'o', ' ', 'b', 'a', 'r']
        func(target_list)
        assert_equal(target_list, ['r', 'a', 'b', ' ', 'o', 'o', 'f'])
        print('Success: test_reverse_inplace')
​
​
def main():
    test = TestReverse()
    reverse_string = ReverseString()
    test.test_reverse(reverse_string.reverse)
    test.test_reverse_inplace(reverse_string.reverse)
​
​
if __name__ == '__main__':
    main()

Q: Problem: Determine if a string s1 is a rotation of another string s2, by calling (only once) a function is_substring.
A: Constraints
- Can we assume the string is ASCII?
  - Yes
  - Note: Unicode strings could require special handling depending on your language
- Is this case sensitive?
  - Yes
- Can we use additional data structures?
  - Yes
- Can we assume this fits in memory?
  - Yes
Test Cases
- Any strings that differ in size -> False
- None, 'foo' -> False (any None results in False)
- ' ', 'foo' -> False
- ' ', ' ' -> True
- 'foobarbaz', 'barbazfoo' -> True
Algorithm
Examine the following test case:
- s1 = 'barbazfoo'
- s2 = 'foobarbaz'
We see that if we can use the given is_substring method if we take compare s2 with s1 + s1:
- s2 = 'foobarbaz'
- s3 = 'barbazfoobarbazfoo'
Complexity:
- Time: O(n)
- Space: O(n)
Code
class Rotation(object):
​
    def is_substring(self, s1, s2):
        return s1 in s2
​
    def is_rotation(self, s1, s2):
        if s1 is None or s2 is None:
            return False
        if len(s1) != len(s2):
            return False
        return self.is_substring(s1, s2 + s2)
Unit Test
%%writefile test_rotation.py
from nose.tools import assert_equal
​
​
class TestRotation(object):
​
    def test_rotation(self):
        rotation = Rotation()
        assert_equal(rotation.is_rotation('o', 'oo'), False)
        assert_equal(rotation.is_rotation(None, 'foo'), False)
        assert_equal(rotation.is_rotation('', 'foo'), False)
        assert_equal(rotation.is_rotation('', ''), True)
        assert_equal(rotation.is_rotation('foobarbaz', 'barbazfoo'), True)
        print('Success: test_rotation')
​
​
def main():
    test = TestRotation()
    test.test_rotation()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement an algorithm to determine if a string has all unique characters.
A: Constraints
- Can we assume the string is ASCII?
  - Yes
  - Note: Unicode strings could require special handling depending on your language
- Can we assume this is case sensitive?
  - Yes
- Can we use additional data structures?
  - Yes
- Can we assume this fits in memory?
  - Yes
Test Cases
- None -> False
- '' -> True
- 'foo' -> False
- 'bar' -> True
Algorithm 1: Sets and Length Comparison
A set is an unordered collection of unique elements.
- If the length of the set(string) equals the length of the string
  - Return True
- Else
  - Return False
Complexity:
- Time: O(n)
- Space: Additional O(n)
Code: Sets and Length Comparison
class UniqueCharsSet(object):
​
    def has_unique_chars(self, string):
        if string is None:
            return False
        return len(set(string)) == len(string)
Algorithm 2: Hash Map Lookup
We'll keep a hash map (set) to keep track of unique characters we encounter.
Steps:
- Scan each character
- For each character:
  - If the character does not exist in a hash map, add the character to a hash map
  - Else, return False
- Return True
Notes:
- We could also use a dictionary, but it seems more logical to use a set as it does not contain duplicate elements
- Since the characters are in ASCII, we could potentially use an array of size 128 (or 256 for extended ASCII)
Complexity:
- Time: O(n)
- Space: Additional O(n)
Code: Hash Map Lookup
class UniqueChars(object):
​
    def has_unique_chars(self, string):
        if string is None:
            return False
        chars_set = set()
        for char in string:
            if char in chars_set:
                return False
            else:
                chars_set.add(char)
        return True
Algorithm 3: In-Place
Assume we cannot use additional data structures, which will eliminate the fast lookup O(1) time provided by our hash map.
- Scan each character
- For each character:
  - Scan all [other] characters in the array
    - Exluding the current character from the scan is rather tricky in Python and results in a non-Pythonic solution
    - If there is a match, return False
- Return True
Algorithm Complexity:
- Time: O(n^2)
- Space: O(1)
Code: In-Place
class UniqueCharsInPlace(object):
​
    def has_unique_chars(self, string):
        if string is None:
            return False
        for char in string:
            if string.count(char) > 1:
                return False
        return True
Unit Test
%%writefile test_unique_chars.py
from nose.tools import assert_equal
​
​
class TestUniqueChars(object):
​
    def test_unique_chars(self, func):
        assert_equal(func(None), False)
        assert_equal(func(''), True)
        assert_equal(func('foo'), False)
        assert_equal(func('bar'), True)
        print('Success: test_unique_chars')
​
​
def main():
    test = TestUniqueChars()
    unique_chars = UniqueChars()
    test.test_unique_chars(unique_chars.has_unique_chars)
    try:
        unique_chars_set = UniqueCharsSet()
        test.test_unique_chars(unique_chars_set.has_unique_chars)
        unique_chars_in_place = UniqueCharsInPlace()
        test.test_unique_chars(unique_chars_in_place.has_unique_chars)
    except NameError:
        # Alternate solutions are only defined
        # in the solutions file
        pass
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement n stacks using a single array.
A: Constraints
- Are the stacks and array a fixed size?
  - Yes
- Are the stacks equally sized?
  - Yes
- Does pushing to a full stack result in an exception?
  - Yes
- Does popping from an empty stack result in an exception?
  - Yes
- Can we assume the user passed in stack index is valid?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- Test the following on the three stacks:
  - Push to full stack -> Exception
  - Push to non-full stack
  - Pop on empty stack -> Exception
  - Pop on non-empty stack
Algorithm
Absolute Index
- return stack size * stack index + stack pointer
Complexity:
- Time: O(1)
- Space: O(1)
Push
- If stack is full, throw exception
- Else
  - Increment stack pointer
  - Get the absolute array index
  - Insert the value to this index
Complexity:
- Time: O(1)
- Space: O(1)
Pop
- If stack is empty, throw exception
- Else
  - Store the value contained in the absolute array index
  - Set the value in the absolute array index to None
  - Decrement stack pointer
  - return value
Complexity:
- Time: O(1)
- Space: O(1)
Code
class Stacks(object):
​
    def __init__(self, num_stacks, stack_size):
        self.num_stacks = num_stacks
        self.stack_size = stack_size
        self.stack_pointers = [-1] * self.num_stacks
        self.stack_array = [None] * self.num_stacks * self.stack_size
​
    def abs_index(self, stack_index):
        return stack_index * self.stack_size + self.stack_pointers[stack_index]
​
    def push(self, stack_index, data):
        if self.stack_pointers[stack_index] == self.stack_size - 1:
            raise Exception('Stack is full')
        self.stack_pointers[stack_index] += 1
        array_index = self.abs_index(stack_index)
        self.stack_array[array_index] = data
​
    def pop(self, stack_index):
        if self.stack_pointers[stack_index] == -1:
            raise Exception('Stack is empty')
        array_index = self.abs_index(stack_index)
        data = self.stack_array[array_index]
        self.stack_array[array_index] = None
        self.stack_pointers[stack_index] -= 1
        return data
Unit Test
%%writefile test_n_stacks.py
from nose.tools import assert_equal
from nose.tools import raises
class TestStacks(object):
    @raises(Exception)
    def test_pop_on_empty(self, num_stacks, stack_size):
        print('Test: Pop on empty stack')
        stacks = Stacks(num_stacks, stack_size)
        stacks.pop(0)
    @raises(Exception)
    def test_push_on_full(self, num_stacks, stack_size):
        print('Test: Push to full stack')
        stacks = Stacks(num_stacks, stack_size)
        for i in range(0, stack_size):
            stacks.push(2, i)
        stacks.push(2, stack_size)
    def test_stacks(self, num_stacks, stack_size):
        print('Test: Push to non-full stack')
        stacks = Stacks(num_stacks, stack_size)
        stacks.push(0, 1)
        stacks.push(0, 2)
        stacks.push(1, 3)
        stacks.push(2, 4)
        print('Test: Pop on non-empty stack')
        assert_equal(stacks.pop(0), 2)
        assert_equal(stacks.pop(0), 1)
        assert_equal(stacks.pop(1), 3)
        assert_equal(stacks.pop(2), 4)
        print('Success: test_stacks\n')
def main():
    num_stacks = 3
    stack_size = 100
    test = TestStacks()
    test.test_pop_on_empty(num_stacks, stack_size)
    test.test_push_on_full(num_stacks, stack_size)
    test.test_stacks(num_stacks, stack_size)
if __name__ == '__main__':
    main()

Q: Problem: Implement a queue using two stacks.
A: Constraints
- Do we expect the methods to be enqueue and dequeue?
  - Yes
- Can we assume we already have a stack class that can be used for this problem?
  - Yes
- Can we push a None value to the Stack?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- Enqueue and dequeue on empty stack
- Enqueue and dequeue on non-empty stack
- Multiple enqueue in a row
- Multiple dequeue in a row
- Enqueue after a dequeue
- Dequeue after an enqueue
Algorithm
We'll use two stacks (left and right) to implement the queue. The left stack will be used for enqueue and the right stack will be used for dequeue.
To prevent multiple dequeue calls from needlessly shifting elements around between the stacks, we'll shift elements in a lazy manner.
Enqueue
- If right stack is not empty
  - Shift the elements of the right stack to the left stack
- Push the data to the left stack
Complexity:
- Time: O(n)
- Space: O(n)
Dequeue
- If the left stack is not empty
  - Shift the elements of the left stack to the right stack
- Pop from the right stack and return the data
Complexity:
- Time: O(n)
- Space: O(n)
Shift Stacks
- While the source stack has elements:
  - Pop from the source stack and push the data to the destination stack
Complexity:
- Time: O(n)
- Space: O(1)
Code
class QueueFromStacks(object):
​
    def __init__(self):
        self.left_stack = Stack()
        self.right_stack = Stack()
​
    def shift_stacks(self, source, destination):
        while source.peek() is not None:
            destination.push(source.pop())
​
    def enqueue(self, data):
        self.shift_stacks(self.right_stack, self.left_stack)
        self.left_stack.push(data)
​
    def dequeue(self):
        self.shift_stacks(self.left_stack, self.right_stack)
        return self.right_stack.pop()
Unit Test
%%writefile test_queue_from_stacks.py
from nose.tools import assert_equal
​
​
class TestQueueFromStacks(object):
​
    def test_queue_from_stacks(self):
        print('Test: Dequeue on empty stack')
        queue = QueueFromStacks()
        assert_equal(queue.dequeue(), None)
​
        print('Test: Enqueue on empty stack')
        print('Test: Enqueue on non-empty stack')
        print('Test: Multiple enqueue in a row')
        num_items = 3
        for i in range(0, num_items):
            queue.enqueue(i)
​
        print('Test: Dequeue on non-empty stack')
        print('Test: Dequeue after an enqueue')
        assert_equal(queue.dequeue(), 0)
​
        print('Test: Multiple dequeue in a row')
        assert_equal(queue.dequeue(), 1)
        assert_equal(queue.dequeue(), 2)
​
        print('Test: Enqueue after a dequeue')
        queue.enqueue(5)
        assert_equal(queue.dequeue(), 5)
​
        print('Success: test_queue_from_stacks')
​
​
def main():
    test = TestQueueFromStacks()
    test.test_queue_from_stacks()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement a queue with enqueue and dequeue methods using a linked list.
A: Constraints
- If there is one item in the list, do you expect the head and tail pointers to both point to it?
  - Yes
- If there are no items on the list, do you expect the head and tail pointers to be None?
  - Yes
- If you dequeue on an empty queue, does that return None?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
Enqueue
- Enqueue to an empty queue
- Enqueue to a non-empty queue
Dequeue
- Dequeue an empty queue -> None
- Dequeue a queue with one element
- Dequeue a queue with more than one element
Algorithm
Enqueue
- If the list is empty, set head and tail to node
- Else, set tail to node
Complexity:
- Time: O(1)
- Space: O(1)
Dequeue
- If the list is empty, return None
- If the list has one node
  - Save the head node's value
  - Set head and tail to None
  - Return the saved value
- Else
  - Save the head node's value
  - Set head to its next node
  - Return the saved value
Complexity:
- Time: O(1)
- Space: O(1)
Code
%%writefile queue_list.py
class Node(object):
​
    def __init__(self, data):
        self.data = data
        self.next = None
​
​
class Queue(object):
​
    def __init__(self):
        self.head = None
        self.tail = None
​
    def enqueue(self, data):
        node = Node(data)
        # Empty list
        if self.head is None and self.tail is None:
            self.head = node
            self.tail = node
        else:
            self.tail.next = node
            self.tail = node
​
    def dequeue(self):
        # Empty list
        if self.head is None and self.tail is None:
            return None
        data = self.head.data
        # Remove only element from a one element list
        if self.head == self.tail:
            self.head = None
            self.tail = None
        else:
            self.head = self.head.next
        return data
Unit Test
%%writefile test_queue_list.py
from nose.tools import assert_equal
​
​
class TestQueue(object):
​
    # TODO: It would be better if we had unit tests for each
    # method in addition to the following end-to-end test
    def test_end_to_end(self):
        print('Test: Dequeue an empty queue')
        queue = Queue()
        assert_equal(queue.dequeue(), None)
​
        print('Test: Enqueue to an empty queue')
        queue.enqueue(1)
​
        print('Test: Dequeue a queue with one element')
        assert_equal(queue.dequeue(), 1)
​
        print('Test: Enqueue to a non-empty queue')
        queue.enqueue(2)
        queue.enqueue(3)
        queue.enqueue(4)
​
        print('Test: Dequeue a queue with more than one element')
        assert_equal(queue.dequeue(), 2)
        assert_equal(queue.dequeue(), 3)
        assert_equal(queue.dequeue(), 4)
​
        print('Success: test_end_to_end')
​
​
def main():
    test = TestQueue()
    test.test_end_to_end()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement SetOfStacks that wraps a list of stacks, where each stack is bound by a capacity.
A: Constraints
- Can we assume we already have a stack class that can be used for this problem?
  - Yes
- Are all stack bound by the same capacity?
  - Yes
- If a stack becomes full, should automatically create one?
  - Yes
- If a stack becomes empty, should we delete it?
  - Yes
- If we pop on an empty stack, should we return None?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- Push and pop on an empty stack
- Push and pop on a non-empty stack
- Push on a capacity stack to create a new one
- Pop on a stack to destroy it
Algorithm
Push
- If there are no stacks or the last stack is full
  - Create a new stack
- Push to the new stack
Complexity:
- Time: O(1)
- Space: O(m), where m is the size of the new stack if the last stack is full
Pop
- If there are no stacks, return None
- Else if the last stack has one element
  - Pop the last element's data
  - Delete the now empty stack
  - Update the last stack pointer
- Else Pop the last element's data
- Return the last element's data
Complexity:
- Time: O(1)
- Space: O(1)
Code
class StackWithCapacity(Stack):
​
    def __init__(self, top=None, capacity=10):
        super(StackWithCapacity, self).__init__(top)
        self.capacity = capacity
        self.num_items = 0
​
    def push(self, data):
        if self.is_full():
            raise Exception('Stack full')
        super(StackWithCapacity, self).push(data)
        self.num_items += 1
​
    def pop(self):
        self.num_items -= 1
        return super(StackWithCapacity, self).pop()
​
    def is_full(self):
        return self.num_items == self.capacity
​
    def is_empty(self):
        return self.num_items == 0
​
​
class SetOfStacks(object):
​
    def __init__(self, indiv_stack_capacity):
        self.indiv_stack_capacity = indiv_stack_capacity
        self.stacks = []
        self.last_stack = None
​
    def push(self, data):
        if self.last_stack is None or self.last_stack.is_full():
            self.last_stack = StackWithCapacity(None, self.indiv_stack_capacity)
            self.stacks.append(self.last_stack)
        self.last_stack.push(data)
​
    def pop(self):
        if self.last_stack is None:
            return None
        data = self.last_stack.pop()
        if self.last_stack.is_empty():
            self.stacks.pop()
            self.last_stack = self.stacks[-1] if self.stacks else None
        return data
Unit Test
%%writefile test_set_of_stacks.py
from nose.tools import assert_equal
​
​
class TestSetOfStacks(object):
​
    def test_set_of_stacks(self):
        print('Test: Push on an empty stack')
        stacks = SetOfStacks(indiv_stack_capacity=2)
        stacks.push(3)
​
        print('Test: Push on a non-empty stack')
        stacks.push(5)
​
        print('Test: Push on a capacity stack to create a new one')
        stacks.push('a')
​
        print('Test: Pop on a stack to destroy it')
        assert_equal(stacks.pop(), 'a')
​
        print('Test: Pop general case')
        assert_equal(stacks.pop(), 5)
        assert_equal(stacks.pop(), 3)
​
        print('Test: Pop on no elements')
        assert_equal(stacks.pop(), None)
​
        print('Success: test_set_of_stacks')
​
​
def main():
    test = TestSetOfStacks()
    test.test_set_of_stacks()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Sort a stack. You can use another stack as a buffer.
A: Constraints
- When sorted, should the largest element be at the top or bottom?
  - Top
- Can you have duplicate values like 5, 5?
  - Yes
- Can we assume we already have a stack class that can be used for this problem?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- Empty stack -> None
- One element stack
- Two or more element stack (general case)
- Already sorted stack
Algorithm
- Our buffer will hold elements in reverse sorted order, smallest at the top
- Store the current top element in a temp variable
- While stack is not empty
  - While buffer is not empty or buffer top is > than temp
    - Move buffer top to stack
  - Move temp to top of buffer
- Return buffer
Complexity:
- Time: O(n^2)
- Space: O(n)
Code
class MyStack(Stack):
​
    def sort(self):
        buff = MyStack()
        while not self.is_empty():
            temp = self.pop()
            if buff.is_empty() or temp >= buff.peek():
                buff.push(temp)
            else:
                while not buff.is_empty() and temp < buff.peek():
                    self.push(buff.pop())
                buff.push(temp)
        return buff
The solution can be further simplified:
class MyStackSimplified(Stack):
​
    def sort(self):
        buff = MyStack()
        while not self.is_empty():
            temp = self.pop()
            while not buff.is_empty() and temp < buff.peek():
                self.push(buff.pop())
            buff.push(temp)
        return buff
Unit Test
%%writefile test_sort_stack.py
from random import randint
from nose.tools import assert_equal
​
​
class TestSortStack(object):
​
    def get_sorted_stack(self, stack, numbers):
        for x in numbers:
            stack.push(x)
        sorted_stack = stack.sort()
        return sorted_stack
​
    def test_sort_stack(self, stack):
        print('Test: Empty stack')
        sorted_stack = self.get_sorted_stack(stack, [])
        assert_equal(sorted_stack.pop(), None)
​
        print('Test: One element stack')
        sorted_stack = self.get_sorted_stack(stack, [1])
        assert_equal(sorted_stack.pop(), 1)
​
        print('Test: Two or more element stack (general case)')
        num_items = 10
        numbers = [randint(0, 10) for x in range(num_items)]
        sorted_stack = self.get_sorted_stack(stack, numbers)
        sorted_numbers = []
        for _ in range(num_items):
            sorted_numbers.append(sorted_stack.pop())
        assert_equal(sorted_numbers, sorted(numbers, reverse=True))
​
        print('Success: test_sort_stack')
​
​
def main():
    test = TestSortStack()
    test.test_sort_stack(MyStack())
    test.test_sort_stack(MyStackSimplified())
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement a stack with push, pop, peek, and is_empty methods using a linked list.
A: Constraints
- If we pop on an empty stack, do we return None?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
Push
- Push to empty stack
- Push to non-empty stack
Pop
- Pop on empty stack
- Pop on single element stack
- Pop on multiple element stack
Peek
- Peek on empty stack
- Peek on one or more element stack
Is Empty
- Is empty on empty stack
- Is empty on one or more element stack
Algorithm
Push
- Create new node with value
- Set node's next to top
- Set top to node
Complexity:
- Time: O(1)
- Space: O(1)
Pop
- If stack is empty, return None
- Else
  - Save top's value
  - Set top to top.next
  - Return saved value
Complexity:
- Time: O(1)
- Space: O(1)
Peek
- If stack is empty, return None
- Else return top's value
Complexity:
- Time: O(1)
- Space: O(1)
Is Empty
- If peek has a value, return False
- Else return True
Complexity:
- Time: O(1)
- Space: O(1)
Code
%%writefile stack.py
class Node(object):
​
    def __init__(self, data, next=None):
        self.data = data
        self.next = next
​
​
class Stack(object):
​
    def __init__(self, top=None):
        self.top = top
​
    def push(self, data):
        self.top = Node(data, self.top)
​
    def pop(self):
        if self.top is None:
            return None
        data = self.top.data
        self.top = self.top.next
        return data
​
    def peek(self):
        return self.top.data if self.top is not None else None
​
    def is_empty(self):
        return self.peek() is None
Unit Test
%%writefile test_stack.py
from nose.tools import assert_equal
​
​
class TestStack(object):
​
    # TODO: It would be better if we had unit tests for each
    # method in addition to the following end-to-end test
    def test_end_to_end(self):
        print('Test: Empty stack')
        stack = Stack()
        assert_equal(stack.peek(), None)
        assert_equal(stack.pop(), None)
​
        print('Test: One element')
        top = Node(5)
        stack = Stack(top)
        assert_equal(stack.pop(), 5)
        assert_equal(stack.peek(), None)
​
        print('Test: More than one element')
        stack = Stack()
        stack.push(1)
        stack.push(2)
        stack.push(3)
        assert_equal(stack.pop(), 3)
        assert_equal(stack.peek(), 2)
        assert_equal(stack.pop(), 2)
        assert_equal(stack.peek(), 1)
        assert_equal(stack.is_empty(), False)
        assert_equal(stack.pop(), 1)
        assert_equal(stack.peek(), None)
        assert_equal(stack.is_empty(), True)
​
        print('Success: test_end_to_end')
​
​
def main():
    test = TestStack()
    test.test_end_to_end()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement a stack with push, pop, and min methods running O(1) time.
A: Constraints
- Can we assume this is a stack of ints?
  - Yes
- Can we assume the input values for push are valid?
  - Yes
- If we call this function on an empty stack, can we return sys.maxsize?
  - Yes
- Can we assume we already have a stack class that can be used for this problem?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- Push/pop on empty stack
- Push/pop on non-empty stack
- Min on empty stack
- Min on non-tempty stack
Algorithm
We'll use a second stack to keep track of the minimum values.
Min
- If the second stack is empty, return an error code (max int value)
- Else, return the top of the stack, without popping it
Complexity:
- Time: O(1)
- Space: O(1)
Push
- Push the data
- If the data is less than min
  - Push data to second stack
Complexity:
- Time: O(1)
- Space: O(1)
Pop
- Pop the data
- If the data is equal to min
  - Pop the top of the second stack
- Return the data
Complexity:
- Time: O(1)
- Space: O(1)
Code
import sys
​
​
class StackMin(Stack):
​
    def __init__(self, top=None):
        super(StackMin, self).__init__(top)
        self.stack_of_mins = Stack()
​
    def minimum(self):
        if self.stack_of_mins.top is None:
            return sys.maxsize
        else:
            return self.stack_of_mins.peek()
​
    def push(self, data):
        super(StackMin, self).push(data)
        if data < self.minimum():
            self.stack_of_mins.push(data)
​
    def pop(self):
        data = super(StackMin, self).pop()
        if data == self.minimum():
            self.stack_of_mins.pop()
        return data
Unit Test
%%writefile test_stack_min.py
from nose.tools import assert_equal
​
​
class TestStackMin(object):
​
    def test_stack_min(self):
        print('Test: Push on empty stack, non-empty stack')
        stack = StackMin()
        stack.push(5)
        assert_equal(stack.peek(), 5)
        assert_equal(stack.minimum(), 5)
        stack.push(1)
        assert_equal(stack.peek(), 1)
        assert_equal(stack.minimum(), 1)
        stack.push(3)
        assert_equal(stack.peek(), 3)
        assert_equal(stack.minimum(), 1)
        stack.push(0)
        assert_equal(stack.peek(), 0)
        assert_equal(stack.minimum(), 0)
​
        print('Test: Pop on non-empty stack')
        assert_equal(stack.pop(), 0)
        assert_equal(stack.minimum(), 1)
        assert_equal(stack.pop(), 3)
        assert_equal(stack.minimum(), 1)
        assert_equal(stack.pop(), 1)
        assert_equal(stack.minimum(), 5)
        assert_equal(stack.pop(), 5)
        assert_equal(stack.minimum(), sys.maxsize)
​
        print('Test: Pop empty stack')
        assert_equal(stack.pop(), None)
​
        print('Success: test_stack_min')
​
​
def main():
    test = TestStackMin()
    test.test_stack_min()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Add two numbers whose digits are stored in a linked list in reverse order.
A: Constraints
- Can we assume this is a non-circular, singly linked list?
  - Yes
- Do we expect the return to be in reverse order too?
  - Yes
- What if one of the inputs is None?
  - Return None for an invalid operation
- How large are these numbers--can they fit in memory?
  - Yes
- Can we assume we already have a linked list class that can be used for this problem?
  - Yes
- Can we assume this fits in memory?
  - Yes
Test Cases
- Empty list(s) -> None
- Add values of different lengths
  - Input 1: 6->5->None
  - Input 2: 9->8->7
  - Result: 5->4->8
- Add values of same lengths
  - Exercised from values of different lengths
  - Done here for completeness
Algorithm
We could solve this with an iterative or a recursive algorithm, both are well suited for this exercise. We'll use a recursive algorithm for practice with recursion. Note this takes an extra space of O(m) where m is the recursion depth.
- Base case:
  - if first and second lists are None AND carry is zero
    - Return None
- Recursive case:
  - Set value to carry
  - Add both nodes' data to value
  - Set the carry to 1 if value >= 10, else 0
  - Set the remainder to value % 10
  - Create a node with the remainder
  - Set node.next to a recursive call on the next nodes, passing in the carry
  - Return node
Complexity:
- Time: O(n)
- Space: O(m), extra space for result and recursion depth
Notes:
- Careful with adding if the lists differ
  - Only add if a node is not None
  - Alternatively, we could add trailing zeroes to the smaller list
Code
class MyLinkedList(LinkedList):
​
    def _add_reverse(self, first_node, second_node, carry):
        # Base case
        if first_node is None and second_node is None and not carry:
            return None
​
        # Recursive case
        value = carry
        value += first_node.data if first_node is not None else 0
        value += second_node.data if second_node is not None else 0
        carry = 1 if value >= 10 else 0
        value %= 10
        node = Node(value)
        node.next = self._add_reverse(
            first_node.next if first_node is not None else None,
            second_node.next if first_node is not None else None,
            carry)
        return node
​
    def add_reverse(self, first_list, second_list):
        # See constraints
        if first_list is None or second_list is None:
            return None
        head = self._add_reverse(first_list.head, second_list.head, 0)
        return MyLinkedList(head)
Unit Test
%%writefile test_add_reverse.py
from nose.tools import assert_equal
​
​
class TestAddReverse(object):
​
    def test_add_reverse(self):
        print('Test: Empty list(s)')
        assert_equal(MyLinkedList().add_reverse(None, None), None)
        assert_equal(MyLinkedList().add_reverse(Node(5), None), None)
        assert_equal(MyLinkedList().add_reverse(None, Node(10)), None)
​
        print('Test: Add values of different lengths')
        # Input 1: 6->5->None
        # Input 2: 9->8->7
        # Result: 5->4->8
        first_list = MyLinkedList(Node(6))
        first_list.append(5)
        second_list = MyLinkedList(Node(9))
        second_list.append(8)
        second_list.append(7)
        result = MyLinkedList().add_reverse(first_list, second_list)
        assert_equal(result.get_all_data(), [5, 4, 8])
​
        print('Test: Add values of same lengths')
        # Input 1: 6->5->4
        # Input 2: 9->8->7
        # Result: 5->4->2->1
        first_head = Node(6)
        first_list = MyLinkedList(first_head)
        first_list.append(5)
        first_list.append(4)
        second_head = Node(9)
        second_list = MyLinkedList(second_head)
        second_list.append(8)
        second_list.append(7)
        result = MyLinkedList().add_reverse(first_list, second_list)
        assert_equal(result.get_all_data(), [5, 4, 2, 1])
​
        print('Success: test_add_reverse')
​
​
def main():
    test = TestAddReverse()
    test.test_add_reverse()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Delete a node in the middle, given only access to that node.
A: Constraints
- Can we assume this is a non-circular, singly linked list?
  - Yes
- What if the final node is being deleted, do we make it a dummy with value None?
  - Yes
- Can we assume we already have a linked list class that can be used for this problem?
  - Yes
Test Cases
- Delete on empty list -> None
- Delete None -> None
- Delete on one node -> [None]
- Delete on multiple nodes
Algorithm
We'll need two pointers, one to the current node and one to the next node. We will copy the next node's data to the current node's data (effectively deleting the current node) and update the current node's next pointer.
- set curr.data to next.data
- set curr.next to next.next
Complexity:
- Time: O(1)
- Space: O(1)
Code
class MyLinkedList(LinkedList):
​
    def delete_node(self, node):
        if node is None:
            return
        if node.next is None:
            node.data = None
        else:
            node.data = node.next.data
            node.next = node.next.next
Unit Test
%%writefile test_delete_mid.py
from nose.tools import assert_equal
​
​
class TestDeleteNode(object):
​
    def test_delete_node(self):
        print('Test: Empty list, null node to delete')
        linked_list = MyLinkedList(None)
        linked_list.delete_node(None)
        assert_equal(linked_list.get_all_data(), [])
​
        print('Test: One node')
        head = Node(2)
        linked_list = MyLinkedList(head)
        linked_list.delete_node(head)
        assert_equal(linked_list.get_all_data(), [None])
​
        print('Test: Multiple nodes')
        linked_list = MyLinkedList(None)
        node0 = linked_list.insert_to_front(2)
        node1 = linked_list.insert_to_front(3)
        node2 = linked_list.insert_to_front(4)
        node3 = linked_list.insert_to_front(1)
        linked_list.delete_node(node1)
        assert_equal(linked_list.get_all_data(), [1, 4, 2])
​
        print('Test: Multiple nodes, delete last element')
        linked_list = MyLinkedList(None)
        node0 = linked_list.insert_to_front(2)
        node1 = linked_list.insert_to_front(3)
        node2 = linked_list.insert_to_front(4)
        node3 = linked_list.insert_to_front(1)
        linked_list.delete_node(node0)
        assert_equal(linked_list.get_all_data(), [1, 4, 3, None])
​
        print('Success: test_delete_node')
​
​
def main():
    test = TestDeleteNode()
    test.test_delete_node()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find the start of a linked list loop.
A: Constraints
- Is this a singly linked list?
  - Yes
- Can we assume we are always passed a circular linked list?
  - No
- When we find a loop, do we return the node or the node's data?
  - The node
- Can we assume we already have a linked list class that can be used for this problem?
  - Yes
Test Cases
- Empty list -> None
- Not a circular linked list -> None
  - One element
  - Two or more elements
- Circular linked list general case
Algorithm
- Use two references slow, fast, initialized to the head
- Increment slow and fast until they meet
  - fast is incremented twice as fast as slow
    - If fast.next is None, we do not have a circular list
- When slow and fast meet, move slow to the head
- Increment slow and fast one node at a time until they meet
- Where they meet is the start of the loop
Complexity:
- Time: O(n)
- Space: O(1)
Code
class MyLinkedList(LinkedList):
​
    def find_loop_start(self):
        if self.head is None or self.head.next is None:
            return None
        slow = self.head
        fast = self.head
        while fast.next is not None:
            slow = slow.next
            fast = fast.next.next
            if fast is None:
                return None
            if slow == fast:
                break
        slow = self.head
        while slow != fast:
            slow = slow.next
            fast = fast.next
            if fast is None:
                return None
        return slow
Unit Test
%%writefile test_find_loop_start.py
from nose.tools import assert_equal
​
​
class TestFindLoopStart(object):
​
    def test_find_loop_start(self):
        print('Test: Empty list')
        linked_list = MyLinkedList()
        assert_equal(linked_list.find_loop_start(), None)
​
        print('Test: Not a circular linked list: One element')
        head = Node(1)
        linked_list = MyLinkedList(head)
        assert_equal(linked_list.find_loop_start(), None)
​
        print('Test: Not a circular linked list: Two elements')
        linked_list.append(2)
        assert_equal(linked_list.find_loop_start(), None)
​
        print('Test: Not a circular linked list: Three or more elements')
        linked_list.append(3)
        assert_equal(linked_list.find_loop_start(), None)
​
        print('Test: General case: Circular linked list')
        node10 = Node(10)
        node9 = Node(9, node10)
        node8 = Node(8, node9)
        node7 = Node(7, node8)
        node6 = Node(6, node7)
        node5 = Node(5, node6)
        node4 = Node(4, node5)
        node3 = Node(3, node4)
        node2 = Node(2, node3)
        node1 = Node(1, node2)
        node0 = Node(0, node1)
        node10.next = node3
        linked_list = MyLinkedList(node0)
        assert_equal(linked_list.find_loop_start(), node3)
​
        print('Success: test_find_loop_start')
​
​
def main():
    test = TestFindLoopStart()
    test.test_find_loop_start()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find the kth to last element of a linked list.
A: Constraints
- Can we assume this is a non-circular, singly linked list?
  - Yes
- Can we assume k is a valid integer?
  - Yes
- If k = 0, does this return the last element?
  - Yes
- What happens if k is greater than or equal to the length of the linked list?
  - Return None
- Can you use additional data structures?
  - No
- Can we assume we already have a linked list class that can be used for this problem?
  - Yes
Test Cases
- Empty list -> None
- k is >= the length of the linked list -> None
- One element, k = 0 -> element
- General case with many elements, k < length of linked list
Algorithm
- Setup two pointers, fast and slow
- Give fast a headstart, incrementing it once if k = 1, twice if k = 2, ...
- Increment both pointers until fast reaches the end
- Return the value of slow
Complexity:
- Time: O(n)
- Space: O(1)
Code
class MyLinkedList(LinkedList):
​
    def kth_to_last_elem(self, k):
        if self.head is None:
            return None
        fast = self.head
        slow = self.head
​
        # Give fast a headstart, incrementing it
        # once for k = 1, twice for k = 2, etc
        for _ in range(k):
            fast = fast.next
            # If k >= num elements, return None
            if fast is None:
                return None
​
        # Increment both pointers until fast reaches the end
        while fast.next is not None:
            fast = fast.next
            slow = slow.next
        return slow.data
Unit Test
%%writefile test_kth_to_last_elem.py
from nose.tools import assert_equal
​
​
class Test(object):
​
    def test_kth_to_last_elem(self):
        print('Test: Empty list')
        linked_list = MyLinkedList(None)
        assert_equal(linked_list.kth_to_last_elem(0), None)
​
        print('Test: k >= len(list)')
        assert_equal(linked_list.kth_to_last_elem(100), None)
​
        print('Test: One element, k = 0')
        head = Node(2)
        linked_list = MyLinkedList(head)
        assert_equal(linked_list.kth_to_last_elem(0), 2)
​
        print('Test: General case')
        linked_list.insert_to_front(1)
        linked_list.insert_to_front(3)
        linked_list.insert_to_front(5)
        linked_list.insert_to_front(7)
        assert_equal(linked_list.kth_to_last_elem(2), 3)
​
        print('Success: test_kth_to_last_elem')
​
​
def main():
    test = Test()
    test.test_kth_to_last_elem()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement a linked list with insert, append, find, delete, length, and print methods.
A: Constraints
- Can we assume this is a non-circular, singly linked list?
  - Yes
- Do we keep track of the tail or just the head?
  - Just the head
- Can we insert None values?
  - No
Test Cases
Insert to Front
- Insert a None
- Insert in an empty list
- Insert in a list with one element or more elements
Append
- Append a None
- Append in an empty list
- Insert in a list with one element or more elements
Find
- Find a None
- Find in an empty list
- Find in a list with one element or more matching elements
- Find in a list with no matches
Delete
- Delete a None
- Delete in an empty list
- Delete in a list with one element or more matching elements
- Delete in a list with no matches
Length
- Length of zero or more elements
Print
- Print an empty list
- Print a list with one or more elements
Algorithm
Insert to Front
- If the data we are inserting is None, return
- Create a node with the input data, set node.next to head
- Assign the head to the node
Complexity:
- Time: O(1)
- Space: O(1)
Append
- If the data we are inserting is None, return
- Create a node with the input data
- If this is an empty list
  - Assign the head to the node
- Else
  - Iterate to the end of the list
  - Set the final node's next to the new node
Complexity:
- Time: O(n)
- Space: O(1)
Find
- If data we are finding is None, return
- If the list is empty, return
- For each node
  - If the value is a match, return it
  - Else, move on to the next node
Complexity:
- Time: O(n)
- Space: O(1)
Delete
- If data we are deleting is None, return
- If the list is empty, return
- For each node, keep track of previous and current node
  - If the value we are deleting is a match in the current node
    - Update previous node's next pointer to the current node's next pointer
    - We do not have have to explicitly delete in Python
  - Else, move on to the next node
- As an alternative, we could avoid the use of two pointers by evaluating the current node's next value:
  - If the next value is a match, set the current node's next to next.next
  - Special care should be taken if deleting the head node
Complexity:
- Time: O(n)
- Space: O(1)
Length
- For each node
  - Increase length counter
Complexity:
- Time: O(n)
- Space: O(1)
Print
- For each node
  - Print the node's value
Complexity:
- Time: O(n)
- Space: O(1)
Code
%%writefile linked_list.py
class Node(object):
​
    def __init__(self, data, next=None):
        self.next = next
        self.data = data
​
    def __str__(self):
        return self.data
​
​
class LinkedList(object):
​
    def __init__(self, head=None):
        self.head = head
​
    def __len__(self):
        curr = self.head
        counter = 0
        while curr is not None:
            counter += 1
            curr = curr.next
        return counter
​
    def insert_to_front(self, data):
        if data is None:
            return None
        node = Node(data, self.head)
        self.head = node
        return node
​
    def append(self, data):
        if data is None:
            return None
        node = Node(data)
        if self.head is None:
            self.head = Node(data)
            return node
        curr_node = self.head
        while curr_node.next is not None:
            curr_node = curr_node.next
        curr_node.next = node
        return node
​
    def find(self, data):
        if data is None:
            return None
        curr_node = self.head
        while curr_node is not None:
            if curr_node.data == data:
                return curr_node
            curr_node = curr_node.next
        return None
​
    def delete(self, data):
        if data is None:
            return
        if self.head is None:
            return
        if self.head.data == data:
            self.head = None
            return
        prev_node = self.head
        curr_node = self.head.next
        while curr_node is not None:
            if curr_node.data == data:
                prev_node.next = curr_node.next
                return
            prev_node = curr_node
            curr_node = curr_node.next
​
    def delete_alt(self, data):
        if data is None:
            return
        if self.head is None:
            return
        curr_node = self.head
        if curr_node.data == data:
            curr_node = curr_node.next
            return
        while curr_node.next is not None:
            if curr_node.next.data == data:
                curr_node.next = curr_node.next.next
                return
            curr_node = curr_node.next
​
    def print_list(self):
        curr_node = self.head
        while curr_node is not None:
            print(curr_node.data)
            curr_node = curr_node.next
​
    def get_all_data(self):
        data = []
        curr_node = self.head
        while curr_node is not None:
            data.append(curr_node.data)
            curr_node = curr_node.next
        return data
Unit Test
%%writefile test_linked_list.py
from nose.tools import assert_equal
​
​
class TestLinkedList(object):
​
    def test_insert_to_front(self):
        print('Test: insert_to_front on an empty list')
        linked_list = LinkedList(None)
        linked_list.insert_to_front(10)
        assert_equal(linked_list.get_all_data(), [10])
​
        print('Test: insert_to_front on a None')
        linked_list.insert_to_front(None)
        assert_equal(linked_list.get_all_data(), [10])
​
        print('Test: insert_to_front general case')
        linked_list.insert_to_front('a')
        linked_list.insert_to_front('bc')
        assert_equal(linked_list.get_all_data(), ['bc', 'a', 10])
​
        print('Success: test_insert_to_front\n')
​
    def test_append(self):
        print('Test: append on an empty list')
        linked_list = LinkedList(None)
        linked_list.append(10)
        assert_equal(linked_list.get_all_data(), [10])
​
        print('Test: append a None')
        linked_list.append(None)
        assert_equal(linked_list.get_all_data(), [10])
​
        print('Test: append general case')
        linked_list.append('a')
        linked_list.append('bc')
        assert_equal(linked_list.get_all_data(), [10, 'a', 'bc'])
​
        print('Success: test_append\n')
​
    def test_find(self):
        print('Test: find on an empty list')
        linked_list = LinkedList(None)
        node = linked_list.find('a')
        assert_equal(node, None)
​
        print('Test: find a None')
        head = Node(10)
        linked_list = LinkedList(head)
        node = linked_list.find(None)
        assert_equal(node, None)
​
        print('Test: find general case with matches')
        head = Node(10)
        linked_list = LinkedList(head)
        linked_list.insert_to_front('a')
        linked_list.insert_to_front('bc')
        node = linked_list.find('a')
        assert_equal(str(node), 'a')
​
        print('Test: find general case with no matches')
        node = linked_list.find('aaa')
        assert_equal(node, None)
​
        print('Success: test_find\n')
​
    def test_delete(self):
        print('Test: delete on an empty list')
        linked_list = LinkedList(None)
        linked_list.delete('a')
        assert_equal(linked_list.get_all_data(), [])
​
        print('Test: delete a None')
        head = Node(10)
        linked_list = LinkedList(head)
        linked_list.delete(None)
        assert_equal(linked_list.get_all_data(), [10])
​
        print('Test: delete general case with matches')
        head = Node(10)
        linked_list = LinkedList(head)
        linked_list.insert_to_front('a')
        linked_list.insert_to_front('bc')
        linked_list.delete('a')
        assert_equal(linked_list.get_all_data(), ['bc', 10])
​
        print('Test: delete general case with no matches')
        linked_list.delete('aa')
        assert_equal(linked_list.get_all_data(), ['bc', 10])
​
        print('Success: test_delete\n')
​
    def test_len(self):
        print('Test: len on an empty list')
        linked_list = LinkedList(None)
        assert_equal(len(linked_list), 0)
​
        print('Test: len general case')
        head = Node(10)
        linked_list = LinkedList(head)
        linked_list.insert_to_front('a')
        linked_list.insert_to_front('bc')
        assert_equal(len(linked_list), 3)
​
        print('Success: test_len\n')
​
​
def main():
    test = TestLinkedList()
    test.test_insert_to_front()
    test.test_append()
    test.test_find()
    test.test_delete()
    test.test_len()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Determine if a linked list is a palindrome.
A: Constraints
- Can we assume this is a non-circular, singly linked list?
  - Yes
- Is a single character or number a palindrome?
  - No
- Can we assume we already have a linked list class that can be used for this problem?
  - Yes
- Can we use additional data structures?
  - Yes
- Can we assume this fits in memory?
  - Yes
Test Cases
- Empty list -> False
- Single element list -> False
- Two or more element list, not a palindrome -> False
- General case: Palindrome with even length -> True
- General case: Palindrome with odd length -> True
Algorithm
- Reverse the linked list
  - Iterate through the current linked list
    - Insert to front the current node into a new linked list
- Compare the reversed list with the original list
  - Only need to compare the first half
Complexity:
- Time: O(n)
- Space: O(n)
Note:
- We could also do this iteratively, using a stack to effectively reverse the first half of the string.
Code
from __future__ import division
​
​
class MyLinkedList(LinkedList):
​
    def is_palindrome(self):
        if self.head is None or self.head.next is None:
            return False
        curr = self.head
        reversed_list = MyLinkedList()
        length = 0
​
        # Reverse the linked list
        while curr is not None:
            reversed_list.insert_to_front(curr.data)
            length += 1
            curr = curr.next
​
        # Compare the reversed list with the original list
        # Only need to compare the first half
        iterations = length // 2
        curr = self.head
        curr_reversed = reversed_list.head
        for _ in range(iterations):
            if curr.data != curr_reversed.data:
                return False
            curr = curr.next
            curr_reversed = curr_reversed.next
        return True
Unit Test
%%writefile test_palindrome.py
from nose.tools import assert_equal
​
​
class TestPalindrome(object):
​
    def test_palindrome(self):
        print('Test: Empty list')
        linked_list = MyLinkedList()
        assert_equal(linked_list.is_palindrome(), False)
​
        print('Test: Single element list')
        head = Node(1)
        linked_list = MyLinkedList(head)
        assert_equal(linked_list.is_palindrome(), False)
​
        print('Test: Two element list, not a palindrome')
        linked_list.append(2)
        assert_equal(linked_list.is_palindrome(), False)
​
        print('Test: General case: Palindrome with even length')
        head = Node('a')
        linked_list = MyLinkedList(head)
        linked_list.append('b')
        linked_list.append('b')
        linked_list.append('a')
        assert_equal(linked_list.is_palindrome(), True)
​
        print('Test: General case: Palindrome with odd length')
        head = Node(1)
        linked_list = MyLinkedList(head)
        linked_list.append(2)
        linked_list.append(3)
        linked_list.append(2)
        linked_list.append(1)
        assert_equal(linked_list.is_palindrome(), True)
​
        print('Success: test_palindrome')
​
​
def main():
    test = TestPalindrome()
    test.test_palindrome()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Partition a linked list around a value x, such that all nodes less than x come before all nodes greater than or equal to x.
A: Constraints
- Can we assume this is a non-circular, singly linked list?
  - Yes
- Do we expect the function to return a new list?
  - Yes
- Can we assume the input x is valid?
  - Yes
- Can we assume we already have a linked list class that can be used for this problem?
  - Yes
- Can we create additional data structures?
  - Yes
- Can we assume this fits in memory?
  - Yes
Test Cases
- Empty list -> []
- One element list -> [element]
- Left linked list is empty
- Right linked list is empty
- General case
  - Partition = 10
  - Input: 4, 3, 13, 8, 10, 1, 10, 12
  - Output: 4, 3, 8, 1, 10, 10, 13, 12
Algorithm
- Create left and right linked lists
- For each element in the list
  - If elem < x, append to the left list
  - else, append to the right list
- Merge left and right lists
Complexity:
- Time: O(n)
- Space: O(n)
Code
class MyLinkedList(LinkedList):
​
    def partition(self, data):
        if self.head is None:
            return
        left = MyLinkedList(None)
        right = MyLinkedList(None)
        curr = self.head
​
        # Build the left and right lists
        while curr is not None:
            if curr.data < data:
                left.append(curr.data)
            elif curr.data == data:
                right.insert_to_front(curr.data)
            else:
                right.append(curr.data)
            curr = curr.next
        curr_left = left.head
        if curr_left is None:
            return right
        else:
            # Merge the two lists
            while curr_left.next is not None:
                curr_left = curr_left.next
            curr_left.next = right.head
            return left
Unit Test
%%writefile test_partition.py
from nose.tools import assert_equal
​
​
class TestPartition(object):
​
    def test_partition(self):
        print('Test: Empty list')
        linked_list = MyLinkedList(None)
        linked_list.partition(10)
        assert_equal(linked_list.get_all_data(), [])
​
        print('Test: One element list, left list empty')
        linked_list = MyLinkedList(Node(5))
        linked_list.partition(0)
        assert_equal(linked_list.get_all_data(), [5])
​
        print('Test: Right list is empty')
        linked_list = MyLinkedList(Node(5))
        linked_list.partition(10)
        assert_equal(linked_list.get_all_data(), [5])
​
        print('Test: General case')
        # Partition = 10
        # Input: 4, 3, 13, 8, 10, 1, 14, 10, 12
        # Output: 4, 3, 8, 1, 10, 10, 13, 14, 12
        linked_list = MyLinkedList(Node(12))
        linked_list.insert_to_front(10)
        linked_list.insert_to_front(14)
        linked_list.insert_to_front(1)
        linked_list.insert_to_front(10)
        linked_list.insert_to_front(8)
        linked_list.insert_to_front(13)
        linked_list.insert_to_front(3)
        linked_list.insert_to_front(4)
        partitioned_list = linked_list.partition(10)
        assert_equal(partitioned_list.get_all_data(),
                     [4, 3, 8, 1, 10, 10, 13, 14, 12])
​
        print('Success: test_partition')
​
​
def main():
    test = TestPartition()
    test.test_partition()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Remove duplicates from a linked list.
A: Constraints
- Can we assume this is a non-circular, singly linked list?
  - Yes
- Can you insert None values in the list?
  - No
- Can we assume we already have a linked list class that can be used for this problem?
  - Yes
- Can we use additional data structures?
  - Implement both solutions
- Can we assume this fits in memory?
  - Yes
Test Cases
- Empty linked list -> []
- One element linked list -> [element]
- General case with no duplicates
- General case with duplicates
Algorithm: Hash Map Lookup
Loop through each node
- For each node
  - If the node's value is in the hash map
    - Delete the node
  - Else
    - Add node's value to the hash map
Complexity:
- Time: O(n)
- Space: O(n)
Algorithm: In-Place
- For each node
  - Compare node with every other node
    - Delete nodes that match current node
Complexity:
- Time: O(n^2)
- Space: O(1)
Note:
- We'll need to use a 'runner' to check every other node and compare it to the current node
Code
class MyLinkedList(LinkedList):
​
    def remove_dupes(self):
        if self.head is None:
            return
        node = self.head
        seen_data = set()
        while node is not None:
            if node.data not in seen_data:
                seen_data.add(node.data)
                prev = node
                node = node.next
            else:
                prev.next = node.next
                node = node.next
​
    def remove_dupes_single_pointer(self):
        if self.head is None:
            return
        node = self.head
        seen_data = set({node.data})
        while node.next is not None:
            if node.next.data in seen_data:
                node.next = node.next.next
            else:
                seen_data.add(node.next.data)
                node = node.next
​
    def remove_dupes_in_place(self):
        curr = self.head
        while curr is not None:
            runner = curr
            while runner.next is not None:
                if runner.next.data == curr.data:
                    runner.next = runner.next.next
                else:
                    runner = runner.next
            curr = curr.next
Unit Test
%%writefile test_remove_duplicates.py
from nose.tools import assert_equal
​
​
class TestRemoveDupes(object):
​
    def test_remove_dupes(self, linked_list):
        print('Test: Empty list')
        linked_list.remove_dupes()
        assert_equal(linked_list.get_all_data(), [])
​
        print('Test: One element list')
        linked_list.insert_to_front(2)
        linked_list.remove_dupes()
        assert_equal(linked_list.get_all_data(), [2])
​
        print('Test: General case, duplicates')
        linked_list.insert_to_front(1)
        linked_list.insert_to_front(1)
        linked_list.insert_to_front(3)
        linked_list.insert_to_front(2)
        linked_list.insert_to_front(3)
        linked_list.insert_to_front(1)
        linked_list.insert_to_front(1)
        linked_list.remove_dupes()
        assert_equal(linked_list.get_all_data(), [1, 3, 2])
​
        print('Test: General case, no duplicates')
        linked_list.remove_dupes()
        assert_equal(linked_list.get_all_data(), [1, 3, 2])
​
        print('Success: test_remove_dupes\n')
​
​
def main():
    test = TestRemoveDupes()
    linked_list = MyLinkedList(None)
    test.test_remove_dupes(linked_list)
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement a binary search tree with an insert method.
A: Constraints
- Can we insert None values?
  - No
- Can we assume we are working with valid integers?
  - Yes
- Can we assume all left descendents <= n < all right descendents?
  - Yes
- Do we have to keep track of the parent nodes?
  - This is optional
- Can we assume this fits in memory?
  - Yes
Test Cases
Insert
Insert will be tested through the following traversal:
In-Order Traversal
- 5, 2, 8, 1, 3 -> 1, 2, 3, 5, 8
- 1, 2, 3, 4, 5 -> 1, 2, 3, 4, 5
If the root input is None, return a tree with the only element being the new root node.
You do not have to code the in-order traversal, it is part of the unit test.
Algorithm
Insert
- If the root is None, return Node(data)
- If the data is <= the current node's data
  - If the current node's left child is None, set it to Node(data)
  - Else, recursively call insert on the left child
- Else
  - If the current node's right child is None, set it to Node(data)
  - Else, recursively call insert on the right child
Complexity:
- Time: O(h), where h is the height of the tree
  - In a balanced tree, the height is O(log(n))
  - In the worst case we have a linked list structure with O(n)
- Space: O(m), where m is the recursion depth, or O(1) if using an iterative approach
Code
%%writefile bst.py
class Node(object):
​
    def __init__(self, data):
        self.data = data
        self.left = None
        self.right = None
        self.parent = None
​
    def __repr__(self):
        return str(self.data)
​
​
class Bst(object):
​
    def __init__(self, root=None):
        self.root = root
​
    def insert(self, data):
        if data is None:
            raise TypeError('data cannot be None')
        if self.root is None:
            self.root = Node(data)
            return self.root
        else:
            return self._insert(self.root, data)
​
    def _insert(self, node, data):
        if node is None:
            return Node(data)
        if data <= node.data:
            if node.left is None:
                node.left = self._insert(node.left, data)
                node.left.parent = node
                return node.left
            else:
                return self._insert(node.left, data)
        else:
            if node.right is None:
                node.right = self._insert(node.right, data)
                node.right.parent = node
                return node.right
            else:
                return self._insert(node.right, data)
Unit Test
%%writefile test_bst.py
from nose.tools import assert_equal
​
​
class TestTree(object):
​
    def __init__(self):
        self.results = Results()
​
    def test_tree_one(self):
        bst = Bst()
        bst.insert(5)
        bst.insert(2)
        bst.insert(8)
        bst.insert(1)
        bst.insert(3)
        in_order_traversal(bst.root, self.results.add_result)
        assert_equal(str(self.results), '[1, 2, 3, 5, 8]')
        self.results.clear_results()
​
    def test_tree_two(self):
        bst = Bst()
        bst.insert(1)
        bst.insert(2)
        bst.insert(3)
        bst.insert(4)
        bst.insert(5)
        in_order_traversal(bst.root, self.results.add_result)
        assert_equal(str(self.results), '[1, 2, 3, 4, 5]')
​
        print('Success: test_tree')
​
​
def main():
    test = TestTree()
    test.test_tree_one()
    test.test_tree_two()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Create a binary search tree with minimal height from a sorted array.
A: Constraints
- Is the array in increasing order?
  - Yes
- Are the array elements unique?
  - Yes
- Can we assume we already have a Node class with an insert method?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- 0, 1, 2, 3, 4, 5, 6 -> height 3
- 0, 1, 2, 3, 4, 5, 6, 7 -> height 4
Algorithm
To create a bst with minimum height, we need to use the middle element as the root. We'll use recursion to divide the array in half and continue to pick the middle element from the left and right halves as the nodes to insert in the tree.
- create_min_bst(array, start, end)
- Base case: Stop when end < start
- Create a node with the mid element
- Recursively build node.left by calling create_min_bst using the left sub array
- Recursively build node.right by calling create_min_bst using the right sub array
- Return the node
Complexity:
- Time: O(n)
- Space: O(h), where h is the tree's height (since this is a tree with minimum height, h = log n)
Code
from __future__ import division
​
​
class MinBst(object):
​
    def create_min_bst(self, array):
        if array is None:
            return
        return self._create_min_bst(array, 0, len(array) - 1)
​
    def _create_min_bst(self, array, start, end):
        if end < start:
            return None
        mid = (start + end) // 2
        node = Node(array[mid])
        node.left = self._create_min_bst(array, start, mid - 1)
        node.right = self._create_min_bst(array, mid + 1, end)
        return node
Unit Test
%%writefile test_bst_min.py
from nose.tools import assert_equal
​
​
def height(node):
    if node is None:
        return 0
    return 1 + max(height(node.left),
                   height(node.right))
​
​
class TestBstMin(object):
​
    def test_bst_min(self):
        min_bst = MinBst()
        array = [0, 1, 2, 3, 4, 5, 6]
        root = min_bst.create_min_bst(array)
        assert_equal(height(root), 3)
​
        min_bst = MinBst()
        array = [0, 1, 2, 3, 4, 5, 6, 7]
        root = min_bst.create_min_bst(array)
        assert_equal(height(root), 4)
​
        print('Success: test_bst_min')
​
​
def main():
    test = TestBstMin()
    test.test_bst_min()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find the second largest node in a binary search tree.
A: Constraints
- If this is called on a None input or a single node, should we raise an exception?
  - Yes
    - None -> TypeError
    - Single node -> ValueError
- Can we assume we already have a Node class with an insert method?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- None or single node -> Exception
Input:
                _10_
              _/    \_
             5        15
            / \       / \
           3   8     12  20
          /     \         \
         2       4        30
Output: 20
Input:
                 10
                 /
                5
               / \
              3   7
Output: 7
Algorithm
If there is no right node, the second largest is the right most left subtree:
                 10
                 /
                5
               / \
              3   7
If there is a right node and the right node has children, recurse to that right child:
                _10_
              _/    \_
             5        15
            / \       / \
           3   8     12  20
          /     \         \
         2       4        30
Eventually we'll get to the following scenario:
                 20
                  \
                   30
If the right node has no children, the second largest is the current node.
Complexity:
- Time: O(h)
- Space: O(h), where h is the height of the tree
Code
class Solution(Bst):
​
    def _find_second_largest(self, node):
        if node.right is not None:
            if node.right.left is not None or node.right.right is not None:
                return self._find_second_largest(node.right)
            else:
                return node
        else:
            return self._find_right_most_node(node.left)
​
    def _find_right_most_node(self, node):
        if node.right is not None:
            return self._find_right_most_node(node.right)
        else:
            return node
​
    def find_second_largest(self):
        if self.root is None:
            raise TypeError('root cannot be None')
        if self.root.right is None and self.root.left is None:
            raise ValueError('root must have at least one child')
        return self._find_second_largest(self.root)
Unit Test
%%writefile test_bst_second_largest.py
from nose.tools import assert_equal, assert_raises
​
​
class TestBstSecondLargest(object):
​
    def test_bst_second_largest(self):
        bst = Solution(None)
        assert_raises(TypeError, bst.find_second_largest)
        root = Node(10)
        bst = Solution(root)
        node5 = bst.insert(5)
        node15 = bst.insert(15)
        node3 = bst.insert(3)
        node8 = bst.insert(8)
        node12 = bst.insert(12)
        node20 = bst.insert(20)
        node2 = bst.insert(2)
        node4 = bst.insert(4)
        node30 = bst.insert(30)
        assert_equal(bst.find_second_largest(), node20)
        root = Node(10)
        bst = Solution(root)
        node5 = bst.insert(5)
        node3 = bst.insert(3)
        node7 = bst.insert(7)
        assert_equal(bst.find_second_largest(), node7)
        print('Success: test_bst_second_largest')
​
​
def main():
    test = TestBstSecondLargest()
    test.test_bst_second_largest()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find the in-order successor of a given node in a binary search tree.
A: Constraints
- If there is no successor, do we return None?
  - Yes
- If the input is None, should we throw an exception?
  - Yes
- Can we assume we already have a Node class that keeps track of parents?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
          _5_
        /     \
       3       8
      / \    /   \
     2   4  6    12
    /        \   / \
   1          7 10  15
               /
              9
In: None  Out: Exception
In: 4     Out: 5
In: 5     Out: 6
In: 8     Out: 9
In: 15    Out: None
Algorithm
- If the node has a right subtree, return the left-most node in the right subtree
- Else, go up until you find a node that is its parent's left node
  - If you get to the root (ie node.parent is None), return None
    - The original input node must be the largest in the tree
  - Else, return the parent
Complexity:
- Time: O(h), where h is the height of the tree
- Space: O(h), where h is the recursion depth (tree height), or O(1) if using an iterative approach
Code
class BstSuccessor(object):
​
    def get_next(self, node):
        if node is None:
            raise TypeError('node cannot be None')
        if node.right is not None:
            return self._left_most(node.right)
        else:
            return self._next_ancestor(node)
​
    def _left_most(self, node):
        if node.left is not None:
            return self._left_most(node.left)
        else:
            return node.data
​
    def _next_ancestor(self, node):
        if node.parent is not None:
            if node.parent.data > node.data:
                return node.parent.data
            else:
                return self._next_ancestor(node.parent)
        # We reached the root, the original input node
        # must be the largest element in the tree.
        return None
Unit Test
%%writefile test_bst_successor.py
from nose.tools import assert_equal
from nose.tools import raises
​
​
class TestBstSuccessor(object):
​
    @raises(Exception)
    def test_bst_successor_empty(self):
        bst_successor = BstSuccessor()
        bst_successor.get_next(None)
​
    def test_bst_successor(self):
        nodes = {}
        node = Node(5)
        nodes[5] = node
        bst = Bst(nodes[5])
        nodes[3] = bst.insert(3)
        nodes[8] = bst.insert(8)
        nodes[2] = bst.insert(2)
        nodes[4] = bst.insert(4)
        nodes[6] = bst.insert(6)
        nodes[12] = bst.insert(12)
        nodes[1] = bst.insert(1)
        nodes[7] = bst.insert(7)
        nodes[10] = bst.insert(10)
        nodes[15] = bst.insert(15)
        nodes[9] = bst.insert(9)
​
        bst_successor = BstSuccessor()
        assert_equal(bst_successor.get_next(nodes[4]), 5)
        assert_equal(bst_successor.get_next(nodes[5]), 6)
        assert_equal(bst_successor.get_next(nodes[8]), 9)
        assert_equal(bst_successor.get_next(nodes[15]), None)
​
        print('Success: test_bst_successor')
​
​
def main():
    test = TestBstSuccessor()
    test.test_bst_successor()
    test.test_bst_successor_empty()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Determine if a tree is a valid binary search tree.
A: Constraints
- Can the tree have duplicates?
  - Yes
- If this is called on a None input, should we raise an exception?
  - Yes
- Can we assume we already have a Node class?
  - Yes
- Can we assume this fits in memory?
  - Yes
Test Cases
None -> exception
Valid:
      5
    /   \
   5     8
  / \   /
 4   6 7
Invalid:
      5
    /   \
   5     8
    \
    20
Algorithm
We'll use a recursive solution that valides left <= current < right, passing down the min and max values as we do a depth-first traversal.
- If the node is None, return True
- If min is set and the node's value <= min, return False
- if max is set and the node's value > max, return False
- Recursively call the validate function on node.left, updating max
- Recursively call the validate function on node.right, updating min
Complexity:
- Time: O(n)
- Space: O(h), where h is the height of the tree
Code
import sys
​
​
class BstValidate(Bst):
​
    def validate(self):
        if self.root is None:
            raise TypeError('No root node')
        return self._validate(self.root,
                              minimum=-sys.maxsize,
                              maximum=sys.maxsize)
​
    def _validate(self, node, minimum, maximum):
        if node is None:
            return True
        if node.data <= minimum or node.data > maximum:
            return False
        if not self._validate(node.left, minimum, node.data):
            return False
        if not self._validate(node.right, node.data, maximum):
            return False
        return True
Unit Test
%%writefile test_bst_validate.py
from nose.tools import assert_equal
from nose.tools import raises
​
​
class TestBstValidate(object):
​
    @raises(Exception)
    def test_bst_validate_empty(self):
        validate_bst(None)
​
    def test_bst_validate(self):
        bst = BstValidate(Node(5))
        bst.insert(8)
        bst.insert(5)
        bst.insert(6)
        bst.insert(4)
        bst.insert(7)
        assert_equal(bst.validate(), True)
​
        bst = BstValidate(Node(5))
        left = Node(5)
        right = Node(8)
        invalid = Node(20)
        bst.root.left = left
        bst.root.right = right
        bst.root.left.right = invalid
        assert_equal(bst.validate(), False)
​
        print('Success: test_bst_validate')
​
​
def main():
    test = TestBstValidate()
    test.test_bst_validate_empty()
    test.test_bst_validate()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Check if a binary tree is balanced.
A: Constraints
- Is a balanced tree one where the heights of two sub trees of any node doesn't differ by more than 1?
  - Yes
- If this is called on a None input, should we raise an exception?
  - Yes
- Can we assume we already have a Node class with an insert method?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> No
- 1 -> Yes
- 5, 3, 8, 1, 4 -> Yes
- 5, 3, 8, 9, 10 -> No
Algorithm
The algorithm will be similar to where we get the height of a tree as seen in here.
However, we could check whether the tree is balanced while also checking for the heights.
- Base case: If the root is None, return 0
- Recursively check whether the left sub tree is balanced, and get its height left_height
- Recursively check whether the right sub tree is balanced, and get its height right_height
- Compare left_height and right_height
- Return 1 + max(left_height, right_height)
Complexity:
- Time: O(n)
- Space: O(h), where h is the height of the tree
Code
class BstBalance(Bst):
​
    def _check_balance(self, node):
        if node is None:
            return 0
        left_height = self._check_balance(node.left)
        if left_height == -1:
            return -1
        right_height = self._check_balance(node.right)
        if right_height == -1:
            return -1
        diff = abs(left_height - right_height)
        if diff > 1:
            return -1
        return 1 + max(left_height, right_height)
​
    def check_balance(self):
        if self.root is None:
            raise TypeError('root cannot be None')
        height = self._check_balance(self.root)
        return height != -1
Unit Test
%%writefile test_check_balance.py
from nose.tools import assert_equal
from nose.tools import raises
​
​
class TestCheckBalance(object):
​
    @raises(TypeError)
    def test_check_balance_empty(self):
        bst = BstBalance(None)
        bst.check_balance()
​
    def test_check_balance(self):
        bst = BstBalance(Node(5))
        assert_equal(bst.check_balance(), True)
​
        bst.insert(3)
        bst.insert(8)
        bst.insert(1)
        bst.insert(4)
        assert_equal(bst.check_balance(), True)
​
        bst = BstBalance(Node(5))
        bst.insert(3)
        bst.insert(8)
        bst.insert(9)
        bst.insert(10)
        assert_equal(bst.check_balance(), False)
​
        bst = BstBalance(Node(3))
        bst.insert(2)
        bst.insert(1)
        bst.insert(5)
        bst.insert(4)
        bst.insert(6)
        bst.insert(7)
        assert_equal(bst.check_balance(), True)
​
        print('Success: test_check_balance')
​
​
def main():
    test = TestCheckBalance()
    test.test_check_balance_empty()
    test.test_check_balance()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement a graph.
A: Constraints
- Is the graph directed?
  - Implement both
- Do the edges have weights?
  - Yes
- Can the graph have cycles?
  - Yes
- If we try to add a node that already exists, do we just do nothing?
  - Yes
- If we try to delete a node that doesn't exist, do we just do nothing?
  - Yes
- Can we assume the inputs are valid?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
Input:
- add_edge(source, destination, weight)
graph.add_edge(0, 1, 5)
graph.add_edge(0, 5, 2)
graph.add_edge(1, 2, 3)
graph.add_edge(2, 3, 4)
graph.add_edge(3, 4, 5)
graph.add_edge(3, 5, 6)
graph.add_edge(4, 0, 7)
graph.add_edge(5, 4, 8)
graph.add_edge(5, 2, 9)
Result:
- source and destination nodes within graph are connected with specified weight.
Note:
- The Graph class will be used as a building block for more complex graph challenges.
Algorithm
Node
Node will keep track of its:
- id
- visit state
- incoming edge count (useful for algorithms such as topological sort)
- adjacent nodes and edge weights
add_neighhbor
- If the neighbor doesn't already exist as an adjacent node
  - Update the adjancet nodes and edge weights
  - Increment the neighbor's incoming edge count
Complexity:
- Time: O(1)
- Space: O(1)
remove_neighhbor
- If the neighbor exists as an adjacent node
  - Decrement the neighbor's incoming edge count
  - Remove the neighbor as an adjacent node
Complexity:
- Time: O(1)
- Space: O(1)
Graph
Graph will keep track of its:
- nodes
add_node
- If node already exists, return it
- Create a node with the given id
- Add the newly created node to the collection of nodes
Complexity:
- Time: O(1)
- Space: O(1)
add_edge
- If the source node is not in the collection of nodes, add it
- If the dest node is not in the collection of nodes, add it
- Add a connection from the source node to the dest node with the given edge weight
add_undirected_edge
- Call add_edge
- Also add a connection from the dest node to the source node with the given edge weight
Complexity:
- Time: O(1)
- Space: O(1)
Code
%%writefile graph.py
from enum import Enum  # Python 2 users: Run pip install enum34
​
​
class State(Enum):
    unvisited = 0
    visiting = 1
    visited = 2
​
​
class Node:
​
    def __init__(self, key):
        self.key = key
        self.visit_state = State.unvisited
        self.incoming_edges = 0
        self.adj_nodes = {}  # Key = key, val = Node
        self.adj_weights = {}  # Key = key, val = weight
​
    def __repr__(self):
        return str(self.key)
​
    def __lt__(self, other):
        return self.key < other.key
​
    def add_neighbor(self, neighbor, weight=0):
        if neighbor is None or weight is None:
            raise TypeError('neighbor or weight cannot be None')
        neighbor.incoming_edges += 1
        self.adj_weights[neighbor.key] = weight
        self.adj_nodes[neighbor.key] = neighbor
​
    def remove_neighbor(self, neighbor):
        if neighbor is None:
            raise TypeError('neighbor cannot be None')
        if neighbor.key not in self.adj_nodes:
            raise KeyError('neighbor not found')
        neighbor.incoming_edges -= 1
        del self.adj_weights[neighbor.key]
        del self.adj_nodes[neighbor.key]
​
​
class Graph:
​
    def __init__(self):
        self.nodes = {}  # Key = key, val = Node
​
    def add_node(self, key):
        if key is None:
            raise TypeError('key cannot be None')
        if key not in self.nodes:
            self.nodes[key] = Node(key)
        return self.nodes[key]
​
    def add_edge(self, source_key, dest_key, weight=0):
        if source_key is None or dest_key is None:
            raise KeyError('Invalid key')
        if source_key not in self.nodes:
            self.add_node(source_key)
        if dest_key not in self.nodes:
            self.add_node(dest_key)
        self.nodes[source_key].add_neighbor(self.nodes[dest_key], weight)
​
    def add_undirected_edge(self, src_key, dst_key, weight=0):
        if src_key is None or dst_key is None:
            raise TypeError('key cannot be None')
        self.add_edge(src_key, dst_key, weight)
        self.add_edge(dst_key, src_key, weight)
Unit Test
%%writefile test_graph.py
from nose.tools import assert_equal
​
​
class TestGraph(object):
​
    def create_graph(self):
        graph = Graph()
        for key in range(0, 6):
            graph.add_node(key)
        return graph
​
    def test_graph(self):
        graph = self.create_graph()
        graph.add_edge(0, 1, weight=5)
        graph.add_edge(0, 5, weight=2)
        graph.add_edge(1, 2, weight=3)
        graph.add_edge(2, 3, weight=4)
        graph.add_edge(3, 4, weight=5)
        graph.add_edge(3, 5, weight=6)
        graph.add_edge(4, 0, weight=7)
        graph.add_edge(5, 4, weight=8)
        graph.add_edge(5, 2, weight=9)
​
        assert_equal(graph.nodes[0].adj_weights[graph.nodes[1].key], 5)
        assert_equal(graph.nodes[0].adj_weights[graph.nodes[5].key], 2)
        assert_equal(graph.nodes[1].adj_weights[graph.nodes[2].key], 3)
        assert_equal(graph.nodes[2].adj_weights[graph.nodes[3].key], 4)
        assert_equal(graph.nodes[3].adj_weights[graph.nodes[4].key], 5)
        assert_equal(graph.nodes[3].adj_weights[graph.nodes[5].key], 6)
        assert_equal(graph.nodes[4].adj_weights[graph.nodes[0].key], 7)
        assert_equal(graph.nodes[5].adj_weights[graph.nodes[4].key], 8)
        assert_equal(graph.nodes[5].adj_weights[graph.nodes[2].key], 9)
​
        assert_equal(graph.nodes[0].incoming_edges, 1)
        assert_equal(graph.nodes[1].incoming_edges, 1)
        assert_equal(graph.nodes[2].incoming_edges, 2)
        assert_equal(graph.nodes[3].incoming_edges, 1)
        assert_equal(graph.nodes[4].incoming_edges, 2)
        assert_equal(graph.nodes[5].incoming_edges, 2)
​
        graph.nodes[0].remove_neighbor(graph.nodes[1])
        assert_equal(graph.nodes[1].incoming_edges, 0)
        graph.nodes[3].remove_neighbor(graph.nodes[4])
        assert_equal(graph.nodes[4].incoming_edges, 1)
​
        assert_equal(graph.nodes[0] < graph.nodes[1], True)
​
        print('Success: test_graph')
​
    def test_graph_undirected(self):
        graph = self.create_graph()
        graph.add_undirected_edge(0, 1, weight=5)
        graph.add_undirected_edge(0, 5, weight=2)
        graph.add_undirected_edge(1, 2, weight=3)
​
        assert_equal(graph.nodes[0].adj_weights[graph.nodes[1].key], 5)
        assert_equal(graph.nodes[1].adj_weights[graph.nodes[0].key], 5)
        assert_equal(graph.nodes[0].adj_weights[graph.nodes[5].key], 2)
        assert_equal(graph.nodes[5].adj_weights[graph.nodes[0].key], 2)
        assert_equal(graph.nodes[1].adj_weights[graph.nodes[2].key], 3)
        assert_equal(graph.nodes[2].adj_weights[graph.nodes[1].key], 3)
​
        print('Success: test_graph_undirected')
​
​
def main():
    test = TestGraph()
    test.test_graph()
    test.test_graph_undirected()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement breadth-first search on a graph.
A: Constraints
- Is the graph directed?
  - Yes
- Can we assume we already have Graph and Node classes?
  - Yes
- Can we assume the inputs are valid?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
Input:
- add_edge(source, destination, weight)
graph.add_edge(0, 1, 5)
graph.add_edge(0, 4, 3)
graph.add_edge(0, 5, 2)
graph.add_edge(1, 3, 5)
graph.add_edge(1, 4, 4)
graph.add_edge(2, 1, 6)
graph.add_edge(3, 2, 7)
graph.add_edge(3, 4, 8)
Result:
- Order of nodes visited: [0, 1, 4, 5, 3, 2]
Algorithm
We generally use breadth-first search to determine the shortest path.
- Add the current node to the queue and mark it as visited
- While the queue is not empty
  - Dequeue a node and visit it
  - Iterate through each adjacent node
    - If the node has not been visited, add it to the queue and mark it as visited
Complexity:
- Time: O(V + E), where V = number of vertices and E = number of edges
- Space: O(V)
Note on space complexity from Wikipedia:
- When the number of vertices in the graph is known ahead of time, and additional data structures are used to determine which vertices have already been added to the queue, the space complexity can be expressed as O(V)
- If the graph is represented by an adjacency list it occupies O(V + E) space in memory
- If the graph is represented by an adjacency matrix representation, it occupies O(V^2)
Code
from collections import deque
​
​
class GraphBfs(Graph):
​
    def bfs(self, root, visit_func):
        if root is None:
            return
        queue = deque()
        queue.append(root)
        root.visit_state = State.visited
        while queue:
            node = queue.popleft()
            visit_func(node)
            for adjacent_node in node.adj_nodes.values():
                if adjacent_node.visit_state == State.unvisited:
                    queue.append(adjacent_node)
                    adjacent_node.visit_state = State.visited
Unit Test
%%writefile test_bfs.py
from nose.tools import assert_equal
​
​
class TestBfs(object):
​
    def __init__(self):
        self.results = Results()
​
    def test_bfs(self):
        nodes = []
        graph = GraphBfs()
        for id in range(0, 6):
            nodes.append(graph.add_node(id))
        graph.add_edge(0, 1, 5)
        graph.add_edge(0, 4, 3)
        graph.add_edge(0, 5, 2)
        graph.add_edge(1, 3, 5)
        graph.add_edge(1, 4, 4)
        graph.add_edge(2, 1, 6)
        graph.add_edge(3, 2, 7)
        graph.add_edge(3, 4, 8)
        graph.bfs(nodes[0], self.results.add_result)
        assert_equal(str(self.results), "[0, 1, 4, 5, 3, 2]")
​
        print('Success: test_bfs')
​
​
def main():
    test = TestBfs()
    test.test_bfs()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find a build order given a list of projects and dependencies.
A: Constraints
- Is it possible to have a cyclic graph as the input?
  - Yes
- Can we assume we already have Graph and Node classes?
  - Yes
- Can we assume the inputs are valid?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- projects: a, b, c, d, e, f, g
- dependencies: (d, g), (f, c), (f, b), (f, a), (c, a), (b, a), (a, e), (b, e)
- output: d, f, c, b, g, a, e
Note: Edge direction is down
    f     d
   /|\    |
  c | b   g
   \|/|
    a |
    |/
    e
Test a graph with a cycle, output should be None
Algorithm
We can determine the build order using a topological sort.
- Build the graph with projects (nodes) and dependencies (directed edges)
- Add initially non-dependent nodes to processed_nodes
  - If none exist, we have a circular dependency, return None
- While the length processed_nodes < the length of graph nodes
  - Remove outgoing edges from newly added items in processed_nodes
  - Add non-dependent nodes to processed_nodes
    - If we didn't add any nodes, we have a circular dependency, return None
- Return processed_nodes
Complexity:
- Time: O(V + E)
- Space: O(V + E)
Code
from collections import deque
​
​
class Dependency(object):
​
    def __init__(self, node_key_before, node_key_after):
        self.node_key_before = node_key_before
        self.node_key_after = node_key_after
class BuildOrder(object):
​
    def __init__(self, dependencies):
        self.dependencies = dependencies
        self.graph = Graph()
        self._build_graph()
​
    def _build_graph(self):
        for dependency in self.dependencies:
            self.graph.add_edge(dependency.node_key_before,
                                dependency.node_key_after)
​
    def _find_start_nodes(self, processed_nodes):
        nodes_to_process = {}
        for key, node in self.graph.nodes.items():
            if node.incoming_edges == 0 and key not in processed_nodes:
                nodes_to_process[key] = node
        return nodes_to_process
​
    def _process_nodes(self, nodes_to_process, processed_nodes):
        for node in nodes_to_process.values():
            # We'll need to iterate on copies since we'll need
            # to change the dictionaries during iteration with
            # the remove_neighbor call
            for adj_node in list(node.adj_nodes.values()):
                node.remove_neighbor(adj_node)
            processed_nodes[node.key] = node
        nodes_to_process = {}
​
    def find_build_order(self):
        result = []
        nodes_to_process = {}
        processed_nodes = {}
        while len(result) != len(self.graph.nodes):
            nodes_to_process = self._find_start_nodes(processed_nodes)
            if not nodes_to_process:
                return None
            result.extend(nodes_to_process.values())
            self._process_nodes(nodes_to_process, processed_nodes)
        return result
Unit Test
%%writefile test_build_order.py
from nose.tools import assert_equal
from nose.tools import assert_true
​
​
class TestBuildOrder(object):
​
    def __init__(self):
        self.dependencies = [
            Dependency('d', 'g'),
            Dependency('f', 'c'),
            Dependency('f', 'b'),
            Dependency('f', 'a'),
            Dependency('c', 'a'),
            Dependency('b', 'a'),
            Dependency('a', 'e'),
            Dependency('b', 'e'),
        ]
​
    def test_build_order(self):
        build_order = BuildOrder(self.dependencies)
        processed_nodes = build_order.find_build_order()
​
        expected_result0 = ('d', 'f')
        expected_result1 = ('c', 'b', 'g')
        assert_true(processed_nodes[0].key in expected_result0)
        assert_true(processed_nodes[1].key in expected_result0)
        assert_true(processed_nodes[2].key in expected_result1)
        assert_true(processed_nodes[3].key in expected_result1)
        assert_true(processed_nodes[4].key in expected_result1)
        assert_true(processed_nodes[5].key is 'a')
        assert_true(processed_nodes[6].key is 'e')
​
        print('Success: test_build_order')
​
    def test_build_order_circular(self):
        self.dependencies.append(Dependency('e', 'f'))
        build_order = BuildOrder(self.dependencies)
        processed_nodes = build_order.find_build_order()
        assert_true(processed_nodes is None)
​
        print('Success: test_build_order_circular')
​
​
def main():
    test = TestBuildOrder()
    test.test_build_order()
    test.test_build_order_circular()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement depth-first search on a graph.
A: Constraints
- Is the graph directed?
  - Yes
- Can we assume we already have Graph and Node classes?
  - Yes
- Can we assume the inputs are valid?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
Input:
- add_edge(source, destination, weight)
graph.add_edge(0, 1, 5)
graph.add_edge(0, 4, 3)
graph.add_edge(0, 5, 2)
graph.add_edge(1, 3, 5)
graph.add_edge(1, 4, 4)
graph.add_edge(2, 1, 6)
graph.add_edge(3, 2, 7)
graph.add_edge(3, 4, 8)
Result:
- Order of nodes visited: [0, 1, 3, 2, 4, 5]
Algorithm
If we want to visit every node in a graph, we generally prefer depth-first search since it is simpler (no need to use a queue). For shortest path, we generally use breadth-first search.
- Visit the current node and mark it visited
- Iterate through each adjacent node
  - If the node has not been visited, call dfs on it
Complexity:
- Time: O(V + E), where V = number of vertices and E = number of edges
- Space: O(V), for the recursion depth
Code
class GraphDfs(Graph):
​
    def dfs(self, root, visit_func):
        if root is None:
            return
        visit_func(root)
        root.visit_state = State.visited
        for node in root.adj_nodes.values():
            if node.visit_state == State.unvisited:
                self.dfs(node, visit_func)
Unit Test
%%writefile test_dfs.py
from nose.tools import assert_equal
​
​
class TestDfs(object):
​
    def __init__(self):
        self.results = Results()
​
    def test_dfs(self):
        nodes = []
        graph = GraphDfs()
        for id in range(0, 6):
            nodes.append(graph.add_node(id))
        graph.add_edge(0, 1, 5)
        graph.add_edge(0, 4, 3)
        graph.add_edge(0, 5, 2)
        graph.add_edge(1, 3, 5)
        graph.add_edge(1, 4, 4)
        graph.add_edge(2, 1, 6)
        graph.add_edge(3, 2, 7)
        graph.add_edge(3, 4, 8)
        graph.dfs(nodes[0], self.results.add_result)
        assert_equal(str(self.results), "[0, 1, 3, 2, 4, 5]")
​
        print('Success: test_dfs')
​
​
def main():
    test = TestDfs()
    test.test_dfs()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Determine whether there is a path between two nodes in a graph.
A: Constraints
- Is the graph directed?
  - Yes
- Can we assume we already have Graph and Node classes?
  - Yes
- Can we assume the inputs are valid?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
Input:
- add_edge(source, destination, weight)
graph.add_edge(0, 1, 5)
graph.add_edge(0, 4, 3)
graph.add_edge(0, 5, 2)
graph.add_edge(1, 3, 5)
graph.add_edge(1, 4, 4)
graph.add_edge(2, 1, 6)
graph.add_edge(3, 2, 7)
graph.add_edge(3, 4, 8)
Result:
- search_path(start=0, end=2) -> True
- search_path(start=0, end=0) -> True
- search_path(start=4, end=5) -> False
Algorithm
To determine if there is a path, we can use either breadth-first or depth-first search.
Breadth-first search can also be used to determine the shortest path. Depth-first search is easier to implement with just straight recursion, but often results in a longer path.
We'll use a breadth-first search approach:
- Add the start node to the queue and mark it as visited
- If the start node is the end node, return True
- While the queue is not empty
  - Dequeue a node and visit it
  - If the node is the end node, return True
  - Iterate through each adjacent node
    - If the node has not been visited, add it to the queue and mark it as visited
- Return False
Complexity:
- Time: O(V + E), where V = number of vertices and E = number of edges
- Space: O(V + E)
Code
from collections import deque
​
​
class GraphPathExists(Graph):
​
    def path_exists(self, start, end):
        if start is None or end is None:
            return False
        if start is end:
            return True
        queue = deque()
        queue.append(start)
        start.visit_state = State.visited
        while queue:
            node = queue.popleft()
            if node is end:
                return True
            for adj_node in node.adj_nodes.values():
                if adj_node.visit_state == State.unvisited:
                    queue.append(adj_node)
                    adj_node.visit_state = State.visited
        return False
Unit Test
%%writefile test_path_exists.py
from nose.tools import assert_equal
​
​
class TestPathExists(object):
​
    def test_path_exists(self):
        nodes = []
        graph = GraphPathExists()
        for id in range(0, 6):
            nodes.append(graph.add_node(id))
        graph.add_edge(0, 1, 5)
        graph.add_edge(0, 4, 3)
        graph.add_edge(0, 5, 2)
        graph.add_edge(1, 3, 5)
        graph.add_edge(1, 4, 4)
        graph.add_edge(2, 1, 6)
        graph.add_edge(3, 2, 7)
        graph.add_edge(3, 4, 8)
​
        assert_equal(graph.path_exists(nodes[0], nodes[2]), True)
        assert_equal(graph.path_exists(nodes[0], nodes[0]), True)
        assert_equal(graph.path_exists(nodes[4], nodes[5]), False)
​
        print('Success: test_path_exists')
​
​
def main():
    test = TestPathExists()
    test.test_path_exists()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find the shortest path between two nodes in a graph.
A: Constraints
- Is this a directional graph?
  - Yes
- Could the graph have cycles?
  - Yes
  - Note: If the answer were no, this would be a DAG.
    - DAGs can be solved with a topological sort
- Are the edges weighted?
  - Yes
  - Note: If the edges were not weighted, we could do a BFS
- Are the edges all non-negative numbers?
  - Yes
  - Note: Graphs with negative edges can be done with Bellman-Ford
    - Graphs with negative cost cycles do not have a defined shortest path
- Do we have to check for non-negative edges?
  - No
- Can we assume the inputs are valid?
  - No
- Can we assume we already have a graph class?
  - Yes
- Can we assume we already have a priority queue class?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
The constaints state we don't have to check for negative edges, so we test with the general case.
graph.add_edge('a', 'b', weight=5)
graph.add_edge('a', 'c', weight=3)
graph.add_edge('a', 'e', weight=2)
graph.add_edge('b', 'd', weight=2)
graph.add_edge('c', 'b', weight=1)
graph.add_edge('c', 'd', weight=1)
graph.add_edge('d', 'a', weight=1)
graph.add_edge('d', 'g', weight=2)
graph.add_edge('d', 'h', weight=1)
graph.add_edge('e', 'a', weight=1)
graph.add_edge('e', 'h', weight=4)
graph.add_edge('e', 'i', weight=7)
graph.add_edge('f', 'b', weight=3)
graph.add_edge('f', 'g', weight=1)
graph.add_edge('g', 'c', weight=3)
graph.add_edge('g', 'i', weight=2)
graph.add_edge('h', 'c', weight=2)
graph.add_edge('h', 'f', weight=2)
graph.add_edge('h', 'g', weight=2)
shortest_path = ShortestPath(graph)
result = shortest_path.find_shortest_path('a', 'i')
assert_equal(result, ['a', 'c', 'd', 'g', 'i'])
assert_equal(shortest_path.path_weight['i'], 8)
Algorithm
Wikipedia's animation:
Initialize the following:
- previous = {} # Key: node key, val: prev node key, shortest path
  - Set each node's previous node key to None
- path_weight = {} # Key: node key, val: weight, shortest path
  - Set each node's shortest path weight to infinity
- remaining = PriorityQueue() # Queue of node key, path weight
  - Add each node's shortest path weight to the priority queue
- Set the start node's path_weight to 0 and update the value in remaining
- Loop while remaining still has items
  - Extract the min node (node with minimum path weight) from remaining
  - Loop through each adjacent node in the min node
    - Calculate the new weight:
      - Adjacent node's edge weight + the min node's path_weight
    - If the newly calculated path is less than the adjacent node's current path_weight:
      - Set the node's previous node key leading to the shortest path
      - Update the adjacent node's shortest path and update the value in the priority queue
- Walk backwards to determine the shortest path:
  - Start at the end node, walk the previous dict to get to the start node
- Reverse the list and return it
Complexity for array-based priority queue:
- Time: O(v^2), where v is the number of vertices
- Space: O(v^2)
This might be better than the min-heap-based variant if the graph has a lot of edges.
O(v^2) is better than O((v + v^2) log v).
Complexity for min-heap-based priority queue:
- Time: O((v + e) log v), where v is the number of vertices, e is the number of edges
- Space: O((v + e) log v)
This might be better than the array-based variant if the graph is sparse.
Code
import sys
​
​
class ShortestPath(object):
​
    def __init__(self, graph):
        if graph is None:
            raise TypeError('graph cannot be None')
        self.graph = graph
        self.previous = {}  # Key: node key, val: prev node key, shortest path
        self.path_weight = {}  # Key: node key, val: weight, shortest path
        self.remaining = PriorityQueue()  # Queue of node key, path weight
        for key in self.graph.nodes.keys():
            # Set each node's previous node key to None
            # Set each node's shortest path weight to infinity
            # Add each node's shortest path weight to the priority queue
            self.previous[key] = None
            self.path_weight[key] = sys.maxsize
            self.remaining.insert(
                PriorityQueueNode(key, self.path_weight[key]))
​
    def find_shortest_path(self, start_node_key, end_node_key):
        if start_node_key is None or end_node_key is None:
            raise TypeError('Input node keys cannot be None')
        if (start_node_key not in self.graph.nodes or
                end_node_key not in self.graph.nodes):
            raise ValueError('Invalid start or end node key')
        # Set the start node's shortest path weight to 0
        # and update the value in the priority queue
        self.path_weight[start_node_key] = 0
        self.remaining.decrease_key(start_node_key, 0)
        while self.remaining:
            # Extract the min node (node with minimum path weight)
            # from the priority queue
            min_node_key = self.remaining.extract_min().obj
            min_node = self.graph.nodes[min_node_key]
            # Loop through each adjacent node in the min node
            for adj_key in min_node.adj_nodes.keys():
                # Node's path:
                # Adjacent node's edge weight + the min node's
                # shortest path weight
                new_weight = (min_node.adj_weights[adj_key] +
                    self.path_weight[min_node_key])
                # Only update if the newly calculated path is
                # less than the existing node's shortest path
                if self.path_weight[adj_key] > new_weight:
                    # Set the node's previous node key leading to the shortest path
                    # Update the adjacent node's shortest path and
                    # update the value in the priority queue
                    self.previous[adj_key] = min_node_key
                    self.path_weight[adj_key] = new_weight
                    self.remaining.decrease_key(adj_key, new_weight)
        # Walk backwards to determine the shortest path:
        # Start at the end node, walk the previous dict to get to the start node
        result = []
        current_node_key = end_node_key
        while current_node_key is not None:
            result.append(current_node_key)
            current_node_key = self.previous[current_node_key]
        # Reverse the list
        return result[::-1]
Unit Test
%%writefile test_shortest_path.py
from nose.tools import assert_equal
​
​
class TestShortestPath(object):
​
    def test_shortest_path(self):
        graph = Graph()
        graph.add_edge('a', 'b', weight=5)
        graph.add_edge('a', 'c', weight=3)
        graph.add_edge('a', 'e', weight=2)
        graph.add_edge('b', 'd', weight=2)
        graph.add_edge('c', 'b', weight=1)
        graph.add_edge('c', 'd', weight=1)
        graph.add_edge('d', 'a', weight=1)
        graph.add_edge('d', 'g', weight=2)
        graph.add_edge('d', 'h', weight=1)
        graph.add_edge('e', 'a', weight=1)
        graph.add_edge('e', 'h', weight=4)
        graph.add_edge('e', 'i', weight=7)
        graph.add_edge('f', 'b', weight=3)
        graph.add_edge('f', 'g', weight=1)
        graph.add_edge('g', 'c', weight=3)
        graph.add_edge('g', 'i', weight=2)
        graph.add_edge('h', 'c', weight=2)
        graph.add_edge('h', 'f', weight=2)
        graph.add_edge('h', 'g', weight=2)
        shortest_path = ShortestPath(graph)
        result = shortest_path.find_shortest_path('a', 'i')
        assert_equal(result, ['a', 'c', 'd', 'g', 'i'])
        assert_equal(shortest_path.path_weight['i'], 8)
​
        print('Success: test_shortest_path')
​
​
def main():
    test = TestShortestPath()
    test.test_shortest_path()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Invert a binary tree.
A: Constraints
- What does it mean to invert a binary tree?
  - Swap all left and right node pairs
- Can we assume we already have a Node class?
  - Yes
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
Input:
     5
   /   \
  2     7
 / \   / \
1   3 6   9
Output:
     5
   /   \
  7     2
 / \   / \
9   6 3   1
Algorithm
- Base case
  - If the root is None, return
- Recursive case
  - Recurse on the left node
  - Recurse on the right node
  - Swap left and right
- Return the node
Complexity:
- Time: O(n)
- Space: O(h), where h is the height, for the recursion depth
Code
class InverseBst(Bst):
​
    def invert_tree(self):
        if self.root is None:
            raise TypeError('root cannot be None')
        return self._invert_tree(self.root)
​
    def _invert_tree(self, root):
        if root is None:
            return
        self._invert_tree(root.left)
        self._invert_tree(root.right)
        root.left, root.right = root.right, root.left
        return root
Unit Test
%%writefile test_invert_tree.py
from nose.tools import assert_equal, assert_raises
​
​
class TestInvertTree(object):
​
    def test_invert_tree(self):
        root = Node(5)
        bst = InverseBst(root)
        node2 = bst.insert(2)
        node3 = bst.insert(3)
        node1 = bst.insert(1)
        node7 = bst.insert(7)
        node6 = bst.insert(6)
        node9 = bst.insert(9)
        result = bst.invert_tree()
        assert_equal(result, root)
        assert_equal(result.left, node7)
        assert_equal(result.right, node2)
        assert_equal(result.left.left, node9)
        assert_equal(result.left.right, node6)
        assert_equal(result.right.left, node3)
        assert_equal(result.right.right, node1)
        print('Success: test_invert_tree')
​
​
def main():
    test = TestInvertTree()
    test.test_invert_tree()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement breadth-first traversal on a binary tree.
A: Constraints
- Can we assume we already have a Node class with an insert method?
  - Yes
- Can we assume this fits in memory?
  - Yes
- What should we do with each node when we process it?
  - Call an input method visit_func on the node
Test Cases
Breadth-First Traversal
- 5, 2, 8, 1, 3 -> 5, 2, 8, 1, 3
Algorithm
- Initialize queue with root
- While queue is not empty
  - Dequeue and print the node
  - Queue the left child
  - Queue the right child
Complexity:
- Time: O(n)
- Space: O(n), extra space for the queue
Code
from collections import deque
​
​
class BstBfs(Bst):
​
    def bfs(self, visit_func):
        if self.root is None:
            raise TypeError('root is None')
        queue = deque()
        queue.append(self.root)
        while queue:
            node = queue.popleft()
            visit_func(node)
            if node.left is not None:
                queue.append(node.left)
            if node.right is not None:
                queue.append(node.right)
Unit Test
%%writefile test_bfs.py
from nose.tools import assert_equal
​
​
class TestBfs(object):
​
    def __init__(self):
        self.results = Results()
​
    def test_bfs(self):
        bst = BstBfs(Node(5))
        bst.insert(2)
        bst.insert(8)
        bst.insert(1)
        bst.insert(3)
        bst.bfs(self.results.add_result)
        assert_equal(str(self.results), '[5, 2, 8, 1, 3]')
​
        print('Success: test_bfs')
​
​
def main():
    test = TestBfs()
    test.test_bfs()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement depth-first traversals (in-order, pre-order, post-order) on a binary tree.
A: Constraints
- Can we assume we already have a Node class with an insert method?
  - Yes
- What should we do with each node when we process it?
  - Call an input method visit_func on the node
- Can we assume this fits in memory?
  - Yes
Test Cases
In-Order Traversal
- 5, 2, 8, 1, 3 -> 1, 2, 3, 5, 8
- 1, 2, 3, 4, 5 -> 1, 2, 3, 4, 5
Pre-Order Traversal
- 5, 2, 8, 1, 3 -> 5, 2, 1, 3, 8
- 1, 2, 3, 4, 5 -> 1, 2, 3, 4, 5
Post-Order Traversal
- 5, 2, 8, 1, 3 -> 1, 3, 2, 8, 5
- 1, 2, 3, 4, 5 -> 5, 4, 3, 2, 1
Algorithm
Test Cases
Note:
- This following are all forms of depth-first traversals
In-Order Traversal
- Recursively call in-order traversal on the left child
- Visit the current node
- Recursively call in-order traversal on the right child
Complexity:
- Time: O(n)
- Space: O(m), where m is the recursion depth, or O(1) if using an iterative approach
Pre-Order Traversal
- Visit the current node
- Recursively call pre-order traversal on the left child
- Recursively call pre-order traversal on the right child
Complexity:
- Time: O(n)
- Space: O(m), where m is the recursion depth, or O(1) if using an iterative approach
Post-Order Traversal
- Recursively call post-order traversal on the left child
- Recursively call post-order traversal on the right child
- Visit the current node
Complexity:
- Time: O(n)
- Space: O(m), where m is the recursion depth, or O(1) if using an iterative approach
Code
class BstDfs(Bst):
​
    def in_order_traversal(self, node, visit_func):
        if node is not None:
            self.in_order_traversal(node.left, visit_func)
            visit_func(node)
            self.in_order_traversal(node.right, visit_func)
​
    def pre_order_traversal(self, node, visit_func):
        if node is not None:
            visit_func(node)
            self.pre_order_traversal(node.left, visit_func)
            self.pre_order_traversal(node.right, visit_func)
​
    def post_order_traversal(self, node, visit_func):
        if node is not None:
            self.post_order_traversal(node.left, visit_func)
            self.post_order_traversal(node.right, visit_func)
            visit_func(node)
Unit Test
%%writefile test_dfs.py
from nose.tools import assert_equal
​
​
class TestDfs(object):
​
    def __init__(self):
        self.results = Results()
​
    def test_dfs(self):
        bst = BstDfs(Node(5))
        bst.insert(2)
        bst.insert(8)
        bst.insert(1)
        bst.insert(3)
​
        bst.in_order_traversal(bst.root, self.results.add_result)
        assert_equal(str(self.results), "[1, 2, 3, 5, 8]")
        self.results.clear_results()
​
        bst.pre_order_traversal(bst.root, self.results.add_result)
        assert_equal(str(self.results), "[5, 2, 1, 3, 8]")
        self.results.clear_results()
​
        bst.post_order_traversal(bst.root, self.results.add_result)
        assert_equal(str(self.results), "[1, 3, 2, 8, 5]")
        self.results.clear_results()
​
        bst = BstDfs(Node(1))
        bst.insert(2)
        bst.insert(3)
        bst.insert(4)
        bst.insert(5)
​
        bst.in_order_traversal(bst.root, self.results.add_result)
        assert_equal(str(self.results), "[1, 2, 3, 4, 5]")
        self.results.clear_results()
​
        bst.pre_order_traversal(bst.root, self.results.add_result)
        assert_equal(str(self.results), "[1, 2, 3, 4, 5]")
        self.results.clear_results()
​
        bst.post_order_traversal(bst.root, self.results.add_result)
        assert_equal(str(self.results), "[5, 4, 3, 2, 1]")
​
        print('Success: test_dfs')
​
​
def main():
    test = TestDfs()
    test.test_dfs()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Determine the height of a tree.
A: Constraints
- Is this a binary tree?
  - Yes
- Can we assume we already have a Node class with an insert method?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- 5 -> 1
- 5, 2, 8, 1, 3 -> 3
Algorithm
We'll use a recursive algorithm.
- If the current node is None, return 0
- Else, return 1 + the maximum height of the left or right children
Complexity:
- Time: O(n)
- Space: O(h), where h is the height of the tree
Code
class BstHeight(Bst):
​
    def height(self, node):
        if node is None:
            return 0
        return 1 + max(self.height(node.left),
                       self.height(node.right))
Unit Test
%%writefile test_height.py
from nose.tools import assert_equal
​
​
class TestHeight(object):
​
    def test_height(self):
        bst = BstHeight(Node(5))
        assert_equal(bst.height(bst.root), 1)
        bst.insert(2)
        bst.insert(8)
        bst.insert(1)
        bst.insert(3)
        assert_equal(bst.height(bst.root), 3)
​
        print('Success: test_height')
​
​
def main():
    test = TestHeight()
    test.test_height()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find the lowest common ancestor of two nodes in a binary tree.
A: Constraints
- Is this a binary search tree?
  - No
- Can we assume the two nodes are in the tree?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
          _10_
        /      \
       5        9
      / \      / \
     12  3    18  20
        / \       /
       1   8     40
- 0, 5 -> None
- 5, 0 -> None
- 1, 8 -> 3
- 12, 8 -> 5
- 12, 40 -> 10
- 9, 20 -> 9
- 3, 5 -> 5
Algorithm
^
* Verify both nodes are in the tree
* Base cases
    * If the root is None, return None
    * If the root is either node, return the root
* Recursively search left and right
* If the left and right are both nodes
    * The return the root
* Else, left or right, whichever is valid
Complexity:
- Time: O(h), where h is the height of the tree
- Space: O(h), where h is the recursion depth
Code
class Node(object):
​
    def __init__(self, key, left=None, right=None):
        self.key = key
        self.left = left
        self.right = right
​
    def __repr__(self):
        return str(self.key)
class BinaryTree(object):
​
    def lca(self, root, node1, node2):
        if None in (root, node1, node2):
            return None
        if (not self._node_in_tree(root, node1) or
                not self._node_in_tree(root, node2)):
            return None
        return self._lca(root, node1, node2)
​
    def _node_in_tree(self, root, node):
        if root is None:
            return False
        if root is node:
            return True
        left = self._node_in_tree(root.left, node)
        right = self._node_in_tree(root.right, node)
        return left or right
​
    def _lca(self, root, node1, node2):
        if root is None:
            return None
        if root is node1 or root is node2:
            return root
        left_node = self._lca(root.left, node1, node2)
        right_node = self._lca(root.right, node1, node2)
        if left_node is not None and right_node is not None:
            return root
        else:
            return left_node if left_node is not None else right_node
class LcaResult(object):
​
    def __init__(self, node, is_ancestor):
        self.node = node
        self.is_ancestor = is_ancestor
​
​
class BinaryTreeOptimized(object):
​
    def lca(self, root, node1, node2):
        if root is None:
            raise TypeError('root cannot be None')
        result = self._lca(root, node1, node2)
        if result.is_ancestor:
            return result.node
        return None
​
    def _lca(self, curr_node, node1, node2):
        if curr_node is None:
            return LcaResult(None, is_ancestor=False)
        if curr_node is node1 and curr_node is node2:
            return LcaResult(curr_node, is_ancestor=True)
        left_result = self._lca(curr_node.left, node1, node2)
        if left_result.is_ancestor:
            return left_result
        right_result = self._lca(curr_node.right, node1, node2)
        if right_result.is_ancestor:
            return right_result
        if left_result.node is not None and right_result.node is not None:
            return LcaResult(curr_node, is_ancestor=True)
        elif curr_node is node1 or curr_node is node2:
            is_ancestor = left_result.node is not None or \
                right_result.node is not None
            return LcaResult(curr_node, is_ancestor)
        else:
            return LcaResult(left_result.node if left_result.node is not None \
                                 else right_result.node, is_ancestor=False)
Unit Test
%%writefile test_lca.py
from nose.tools import assert_equal
​
​
class TestLowestCommonAncestor(object):
​
    def test_lca(self):
        node10 = Node(10)
        node5 = Node(5)
        node12 = Node(12)
        node3 = Node(3)
        node1 = Node(1)
        node8 = Node(8)
        node9 = Node(9)
        node18 = Node(18)
        node20 = Node(20)
        node40 = Node(40)
        node3.left = node1
        node3.right = node8
        node5.left = node12
        node5.right = node3
        node20.left = node40
        node9.left = node18
        node9.right = node20
        node10.left = node5
        node10.right = node9
        root = node10
        node0 = Node(0)
        binary_tree = BinaryTree()
        assert_equal(binary_tree.lca(root, node0, node5), None)
        assert_equal(binary_tree.lca(root, node5, node0), None)
        assert_equal(binary_tree.lca(root, node1, node8), node3)
        assert_equal(binary_tree.lca(root, node12, node8), node5)
        assert_equal(binary_tree.lca(root, node12, node40), node10)
        assert_equal(binary_tree.lca(root, node9, node20), node9)
        assert_equal(binary_tree.lca(root, node3, node5), node5)
        print('Success: test_lca')
​
​
def main():
    test = TestLowestCommonAncestor()
    test.test_lca()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Create a list for each level of a binary tree.
A: Constraints
- Is this a binary search tree?
  - Yes
- Should each level be a list of nodes?
  - Yes
- Can we assume we already have a Node class with an insert method?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- 5, 3, 8, 2, 4, 1, 7, 6, 9, 10, 11 -> [[5], [3, 8], [2, 4, 7, 9], [1, 6, 10], [11]]
Note: Each number in the result is actually a node containing the number
Algorithm
We can use either a depth-first or a breadth-first search. Intuitively, it seems like a breadth-first search might be a better fit as we are creating a linked list for each level.
We can use a modified breadth-first search that keeps track of parents as we build the linked list for the current level.
- Append the root to the current level's linked list current
- While the current is not empty:
  - Add current to results
  - Set parents to current to prepare to go one level deeper
  - Clear current so it can hold the next level
  - For each parent in parents, add the children to current
- Return the results
Complexity:
- Time: O(n)
- Space: O(n)
Code
class BstLevelLists(Bst):
​
    def create_level_lists(self):
        if self.root is None:
            return
        results = []
        current = []
        parents = []
        current.append(self.root)
        while current:
            results.append(current)
            parents = list(current)
            current = []
            for parent in parents:
                if parent.left is not None:
                    current.append(parent.left)
                if parent.right is not None:
                    current.append(parent.right)
        return results
Unit Test
%%writefile test_tree_level_lists.py
from nose.tools import assert_equal
​
​
class TestTreeLevelLists(object):
​
    def test_tree_level_lists(self):
        bst = BstLevelLists(Node(5))
        bst.insert(3)
        bst.insert(8)
        bst.insert(2)
        bst.insert(4)
        bst.insert(1)
        bst.insert(7)
        bst.insert(6)
        bst.insert(9)
        bst.insert(10)
        bst.insert(11)
​
        levels = bst.create_level_lists()
        results_list = []
        for level in levels:
            results = Results()
            for node in level:
                results.add_result(node)
            results_list.append(results)
​
        assert_equal(str(results_list[0]), '[5]')
        assert_equal(str(results_list[1]), '[3, 8]')
        assert_equal(str(results_list[2]), '[2, 4, 7, 9]')
        assert_equal(str(results_list[3]), '[1, 6, 10]')
        assert_equal(str(results_list[4]), '[11]')
​
        print('Success: test_tree_level_lists')
​
​
def main():
    test = TestTreeLevelLists()
    test.test_tree_level_lists()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement a trie with find, insert, remove, and list_words methods.
A: Constraints
- Can we assume we are working with strings?
  - Yes
- Are the strings in ASCII?
  - Yes
- Should find only match exact words with a terminating character?
  - Yes
- Should list_words only return words with a terminating character?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
root node is denoted by ''
         ''
       /  |  \
      h   a*  m
     / \   \   \
    a   e*  t*  e*
   / \         / \
  s*  t*      n*  t*
             /
            s*
find
* Find on an empty trie
* Find non-matching
* Find matching
insert
* Insert on empty trie
* Insert to make a leaf terminator char
* Insert to extend an existing terminator char
remove
* Remove me
* Remove mens
* Remove a
* Remove has
list_words
* List empty
* List general case
Algorithm
find
- Set node to the root
- For each char in the input word
  - Check the current node's children to see if it contains the char
    - If a child has the char, set node to the child
    - Else, return None
- Return the last child node if it has a terminator, else None
Complexity:
- Time: O(m), where m is the length of the word
- Space: O(h) for the recursion depth (tree height), or O(1) if using an iterative approach
insert
- set node to the root
- For each char in the input word
  - Check the current node's children to see if it contains the char
    - If a child has the char, set node to the child
    - Else, insert a new node with the char
      - Update children and parents
- Set the last node as a terminating node
Complexity:
- Time: O(m), where m is the length of the word
- Space: O(h) for the recursion depth (tree height), or O(1) if using an iterative approach
remove
- Determine the matching terminating node by calling the find method
- If the matching node has children, remove the terminator to prevent orphaning its children
- Set the parent node to the matching node's parent
- We'll be looping up the parent chain to propagate the delete
- While the parent is valid
  - If the node has children
    - Return to prevent orphaning its remaining children
  - If the node is a terminating node and it isn't the original matching node from the find call
    - Return to prevent deleting this additional valid word
  - Remove the parent node's child entry matching the node
  - Set the node to the parent
  - Set the parent to the parent's parent
Complexity:
- Time: O(m+h), where where m is the length of the word and h is the tree height
- Space: O(h) for the recursion depth (tree height), or O(1) if using an iterative approach
list_words
- Do a pre-order traversal, passing down the current word
  - When you reach a terminating node, add it to the list of results
Complexity:
- Time: O(n)
- Space: O(h) for the recursion depth (tree height), or O(1) if using an iterative approach
Code
%%writefile trie.py
from collections import OrderedDict
​
​
class Node(object):
​
    def __init__(self, key, parent=None, terminates=False):
        self.key = key
        self.terminates = False
        self.parent = parent
        self.children = {}
​
​
class Trie(object):
​
    def __init__(self):
        self.root = Node('')
​
    def find(self, word):
        if word is None:
            raise TypeError('word cannot be None')
        node = self.root
        for char in word:
            if char in node.children:
                node = node.children[char]
            else:
                return None
        return node if node.terminates else None
​
    def insert(self, word):
        if word is None:
            raise TypeError('word cannot be None')
        node = self.root
        parent = None
        for char in word:
            if char in node.children:
                node = node.children[char]
            else:
                node.children[char] = Node(char, parent=node)
                node = node.children[char]
        node.terminates = True
​
    def remove(self, word):
        if word is None:
            raise TypeError('word cannot be None')
        node = self.find(word)
        if node is None:
            raise KeyError('word does not exist')
        node.terminates = False
        parent = node.parent
        while parent is not None:
            # As we are propagating the delete up the
            # parents, if this node has children, stop
            # here to prevent orphaning its children.
            # Or
            # if this node is a terminating node that is
            # not the terminating node of the input word,
            # stop to prevent removing the associated word.
            if node.children or node.terminates:
                return
            del parent.children[node.key]
            node = parent
            parent = parent.parent
​
    def list_words(self):
        result = []
        curr_word = ''
        self._list_words(self.root, curr_word, result)
        return result
​
    def _list_words(self, node, curr_word, result):
        if node is None:
            return
        for key, child in node.children.items():
            if child.terminates:
                result.append(curr_word + key)
            self._list_words(child, curr_word + key, result)
Unit Test
%%writefile test_trie.py
from nose.tools import assert_true
from nose.tools import raises
​
​
class TestTrie(object):
​
    def test_trie(self):
        trie = Trie()
​
        print('Test: Insert')
        words = ['a', 'at', 'has', 'hat', 'he',
                 'me', 'men', 'mens', 'met']
        for word in words:
            trie.insert(word)
        for word in trie.list_words():
            assert_true(trie.find(word) is not None)
        print('Test: Remove me')
        trie.remove('me')
        words_removed = ['me']
        words = ['a', 'at', 'has', 'hat', 'he',
                 'men', 'mens', 'met']
        for word in words:
            assert_true(trie.find(word) is not None)
        for word in words_removed:
            assert_true(trie.find(word) is None)
​
        print('Test: Remove mens')
        trie.remove('mens')
        words_removed = ['me', 'mens']
        words = ['a', 'at', 'has', 'hat', 'he',
                 'men', 'met']
        for word in words:
            assert_true(trie.find(word) is not None)
        for word in words_removed:
            assert_true(trie.find(word) is None)
​
        print('Test: Remove a')
        trie.remove('a')
        words_removed = ['a', 'me', 'mens']
        words = ['at', 'has', 'hat', 'he',
                 'men', 'met']
        for word in words:
            assert_true(trie.find(word) is not None)
        for word in words_removed:
            assert_true(trie.find(word) is None)
​
        print('Test: Remove has')
        trie.remove('has')
        words_removed = ['a', 'has', 'me', 'mens']
        words = ['at', 'hat', 'he',
                 'men', 'met']
        for word in words:
            assert_true(trie.find(word) is not None)
        for word in words_removed:
            assert_true(trie.find(word) is None)
​
        print('Success: test_trie')
​
    @raises(Exception)
    def test_trie_remove_invalid(self):
        print('Test: Remove from empty trie')
        trie = Trie()
        assert_true(trie.remove('foo') is None)
​
​
def main():
    test = TestTrie()
    test.test_trie()
    test.test_trie_remove_invalid()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement common bit manipulation operations: get_bit, set_bit, clear_bit, clear_bits_msb_to_index, clear_bits_msb_to_lsb, update_bit.
A: Constraints
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None as a number input -> Exception
- Negative index -> Exception
get_bit
number   = 0b10001110, index = 3
expected = True
set_bit
number   = 0b10001110, index = 4
expected = 0b10011110
clear_bit
number   = 0b10001110, index = 3
expected = 0b10000110
clear_bits_msb_to_index
number   = 0b10001110, index = 3
expected = 0b00000110
clear_bits_index_to_lsb
number   = 0b10001110, index = 3
expected = 0b10000000
update_bit
number   = 0b10001110, index = 3, value = 1
expected = 0b10001110
number   = 0b10001110, index = 3, value = 0
expected = 0b10000110
number   = 0b10001110, index = 0, value = 1
expected = 0b10001111
Algorithm
get_bit
indices  7 6 5 4  3 2 1 0  index = 3
--------------------------------------------------
input    1 0 0 0  1 1 1 0  0b10001110
mask     0 0 0 0  1 0 0 0  1 << index
--------------------------------------------------
result   0 0 0 0  1 0 0 0  number & mask != 0
Complexity:
- Time: O(b)
- Space: O(b)
set_bit
indices  7 6 5 4  3 2 1 0  index = 4
--------------------------------------------------
input    1 0 0 0  1 1 1 0  0b10001110
mask     0 0 0 1  0 0 0 0  1 << index
--------------------------------------------------
result   1 0 0 1  1 1 1 0  number | mask
Complexity:
- Time: O(b)
- Space: O(b)
clear_bit
indices  7 6 5 4  3 2 1 0  index = 3
--------------------------------------------------
input    1 0 0 0  1 1 1 0  0b10001110
mask     0 0 0 0  1 0 0 0  1 << index
mask     1 1 1 1  0 1 1 1  ~(1 << index)
--------------------------------------------------
result   1 0 0 0  0 1 1 0  number & mask
Complexity:
- Time: O(b)
- Space: O(b)
clear_bits_msb_to_index
indices  7 6 5 4  3 2 1 0  index = 3
--------------------------------------------------
input    1 0 0 0  1 1 1 0  0b10001110
mask     0 0 0 0  1 0 0 0  1 << index
mask     0 0 0 0  0 1 1 1  (1 << index) - 1
--------------------------------------------------
result   0 0 0 0  0 1 1 1  number & mask
Complexity:
- Time: O(b)
- Space: O(b)
clear_bits_index_to_lsb
indices  7 6 5 4  3 2 1 0  index = 3
--------------------------------------------------
input    1 0 0 0  1 1 1 0  0b10001110
mask     0 0 0 1  0 0 0 0  1 << index + 1
mask     0 0 0 0  1 1 1 1  (1 << index + 1) - 1
mask     1 1 1 1  0 0 0 0  ~((1 << index + 1) - 1)
--------------------------------------------------
result   1 0 0 0  0 0 0 0  number & mask
Complexity:
- Time: O(b)
- Space: O(b)
update_bit
- Use get_bit to see if the value is already set or cleared
- If not, use set_bit if setting the value or clear_bit if clearing the value
Complexity:
- Time: O(b)
- Space: O(b)
Code
def validate_index(func):
    def validate_index_wrapper(self, *args, **kwargs):
        for arg in args:
            if arg < 0:
                raise IndexError('Invalid index')
        return func(self, *args, **kwargs)
    return validate_index_wrapper
class Bit(object):
​
    def __init__(self, number):
        if number is None:
            raise TypeError('number cannot be None')
        self.number = number
​
    @validate_index
    def get_bit(self, index):
        mask = 1 << index
        return self.number & mask != 0
​
    @validate_index
    def set_bit(self, index):
        mask = 1 << index
        self.number |= mask
        return self.number
​
    @validate_index
    def clear_bit(self, index):
        mask = ~(1 << index)
        self.number &= mask
        return self.number
​
    @validate_index
    def clear_bits_msb_to_index(self, index):
        mask = (1 << index) - 1
        self.number &= mask
        return self.number
​
    @validate_index
    def clear_bits_index_to_lsb(self, index):
        mask = ~((1 << index + 1) - 1)
        self.number &= mask
        return self.number
​
    @validate_index
    def update_bit(self, index, value):
        if value is None or value not in (0, 1):
            raise Exception('Invalid value')
        if self.get_bit(index) == value:
            return self.number
        if value:
            self.set_bit(index)
        else:
            self.clear_bit(index)
        return self.number
Unit Test
%%writefile test_bit.py
from nose.tools import assert_equal
​
​
class TestBit(object):
​
    def test_bit(self):
        number = int('10001110', base=2)
        bit = Bit(number)
        assert_equal(bit.get_bit(index=3), True)
        expected = int('10011110', base=2)
        assert_equal(bit.set_bit(index=4), expected)
        bit = Bit(number)
        expected = int('10000110', base=2)
        assert_equal(bit.clear_bit(index=3), expected)
        bit = Bit(number)
        expected = int('00000110', base=2)
        assert_equal(bit.clear_bits_msb_to_index(index=3), expected)
        bit = Bit(number)
        expected = int('10000000', base=2)
        assert_equal(bit.clear_bits_index_to_lsb(index=3), expected)
        bit = Bit(number)
        assert_equal(bit.update_bit(index=3, value=1), number)
        bit = Bit(number)
        expected = int('10000110', base=2)
        assert_equal(bit.update_bit(index=3, value=0), expected)
        bit = Bit(number)
        expected = int('10001111', base=2)
        assert_equal(bit.update_bit(index=0, value=1), expected)
        print('Success: test_bit')
​
​
def main():
    test = TestBit()
    test.test_bit()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Determine the number of bits to flip to convert int a to int b.
A: Constraints
- Can we assume A and B are always ints?
  - Yes
- Is the output an int?
  - Yes
- Can we assume A and B are always the same number of bits?
  - Yes
- Can we assume the inputs are valid (not None)?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- A or B is None -> Exception
- General case A = 11101
- B = 01111
- Result: 2
Algorithm
We can use the xor operator to determine the bit differences between a and b
- Set count to 0
- Set c to a xor b
- Loop while c != 0:
  - Increment count if the LSB in c is a 1
    - We can check this by using a mask of 1
  - Right shift c by 1
- Return the count
Complexity:
- Time: O(b), where b is the number of bits
- Space: O(b), where b is the number of bits
Code
class Bits(object):
​
    def bits_to_flip(self, a, b):
        if a is None or b is None:
            raise TypeError('a or b cannot be None')
        count = 0
        c = a ^ b
        while c:
            count += c & 1
            c >>= 1
        return count
Unit Test
%%writefile test_bits_to_flip.py
from nose.tools import assert_equal
​
​
class TestBits(object):
​
    def test_bits_to_flip(self):
        bits = Bits()
        a = int('11101', base=2)
        b = int('01111', base=2)
        expected = 2
        assert_equal(bits.bits_to_flip(a, b), expected)
        print('Success: test_bits_to_flip')
​
​
def main():
    test = TestBits()
    test.test_bits_to_flip()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement the method draw_line(screen, width, x1, x2) where screen is a list of bytes, width is divisible by 8, and x1, x2 are absolute pixel positions.
A: Constraints
- Can we assume the inputs are valid?
  - No
- For the output, do we set corresponding bits in screen?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- Invalid inputs -> Exception
  - screen is empty
  - width = 0
  - any input param is None
  - x1 or x2 is out of bounds
- General case for len(screen) = 20, width = 32:
  - x1 = 2, x2 = 6
    - screen[0] = int('00111110', base=2)
  - x1 = 68, x2 = 80
    - screen[8], int('00001111', base=2)
    - screen[9], int('11111111', base=2)
    - screen[10], int('10000000', base=2)
Algorithm
- Set start offset to x1 % 8
- Set end offset to x2 % 8
- Determine where the first and last full bytes are
  - First full byte = x1 // 8
    - Increment by 1 if start offset != 0
  - Last full byte = x2 // 8
    - Decrement by 1 if end offset != 7
  - Fill the bytes with 1111 1111
- If x1 and x2 are in the same byte
    x1 = 2
    x2 = 6
    index  0123 4567
    byte   0011 1110
    We want to build left and right masks such that:
    left   0011 1111
    right  1111 1110
    ----------------
           0011 1110  left & right
    Build the left mask:
    left   0000 0001  1
           0100 0000  1 << (8 - x1)
           0011 1111  (1 << (8 - x1)) - 1
    Build the right mask:
    right  0000 0000  1
           0000 0010  1 << (8 - x2 - 1)
           0000 0001  (1 << (8 - x2 - 1) - 1
           1111 1110  ~((1 << (8 - x2 - 1) - 1)
    Combine the left and right masks:
    left   0011 1111
    right  1111 1110
    ----------------
           0011 1110  left & right
    Set screen[x1//8] or screen[x2//8] |= mask
       * Else
    If our starting partial byte is:
           0000 1111
    Build start mask:
    start  0000 0001  1
           0001 0000  1 << 1 << start offset
           0000 1111  (1 << 1 << start offset) - 1
    If our ending partial byte is:
           1111 1100
    Build end mask:
    end    1000 0000  0x10000000
           1111 1100  0x10000000 >> end offset
    Set screen[x1//8] |= start mask
    Set screen[x2//8] |= end mask
Complexity:
- Time: O(length of screen)
- Space: O(1)
Code
class BitsScreen(object):
​
    def draw_line(self, screen, width, x1, x2):
        if None in (screen, width, x1, x2):
            raise TypeError('Invalid argument: None')
        if not screen or not width:
            raise ValueError('Invalid arg: Empty screen or width')
        MAX_BIT_VALUE = len(screen) * 8
        if x1 < 0 or x2 < 0 or x1 >= MAX_BIT_VALUE or x2 >= MAX_BIT_VALUE:
            raise ValueError('Invalid arg: x1 or x2 out of bounds')
        start_bit = x1 % 8
        end_bit = x2 % 8
        first_full_byte = x1 // 8
        if start_bit != 0:
            first_full_byte += 1
        last_full_byte = x2 // 8
        if end_bit != (8 - 1):
            last_full_byte -= 1
        for byte in range(first_full_byte, last_full_byte + 1):
            screen[byte] = int('11111111', base=2)
        start_byte = x1 // 8
        end_byte = x2 // 8
        if start_byte == end_byte:
            left_mask = (1 << (8 - start_bit)) - 1
            right_mask = ~((1 << (8 - end_bit - 1)) - 1)
            mask = left_mask & right_mask
            screen[start_byte] |= mask
        else:
            start_mask = (1 << (8 - start_bit)) - 1
            end_mask = 1 << (8 - end_bit - 1)
            screen[start_byte] |= start_mask
            screen[end_byte] |= end_mask
Unit Test
%%writefile test_draw_line.py
from nose.tools import assert_equal
​
​
class TestBitsScreen(object):
​
    def test_draw_line(self):
        bits_screen = BitsScreen()
        screen = []
        for _ in range(20):
            screen.append(int('00000000', base=2))
        bits_screen.draw_line(screen, width=32, x1=68, x2=80)
        assert_equal(screen[8], int('00001111', base=2))
        assert_equal(screen[9], int('11111111', base=2))
        assert_equal(screen[10], int('10000000', base=2))
        bits_screen.draw_line(screen, width=32, x1=2, x2=6)
        assert_equal(screen[0], int('00111110', base=2))
        bits_screen.draw_line(screen, width=32, x1=10, x2=13)
        assert_equal(screen[1], int('00111100', base=2))
        print('Success: test_draw_line')
​
​
def main():
    test = TestBitsScreen()
    test.test_draw_line()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Flip one bit from 0 to 1 to maximize the longest sequence of 1s.
A: Constraints
- Is the input an int, base 2?
  - Yes
- Can we assume the input is a 32 bit number?
  - Yes
- Do we have to validate the length of the input?
  - No
- Is the output an int?
  - Yes
- Can we assume the inputs are valid?
  - No
- Can we assume we are using a positive number since Python doesn't have an >>> operator?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> Exception
- All 1's -> Count of 1s
- All 0's -> 1
- General case
  - Trailing zeroes
    - 0000 1111 1101 1101 1111 0011 1111 0000 -> 10 (ten)
  - Trailing ones
    - 0000 1001 1101 1101 1111 0001 1111 0111 -> 9
Algorithm
- seen = []
- Build a list of sequence counts
  - Look for 0's
    - This will be 0 length if the input has trailing ones
    - Add sequence length to seen
  - Look for 1's
    - Add sequence length to seen
- Find the largest sequence of ones looking at seen
  - Loop through seen
    - On each iteration of the loop, flip what we are looking for from 0 to 1 and vice versa
    - If seen[i] represents 1's, continue, we only want to process 0's
    - If this is our first iteration:
      - max_result = seen[i+1] + 1 if seen[i] > 0
      - continue
    - If we are looking at leading zeroes (i == len(seen)-1):
      - result = seen[i-1] + 1
    - If we are looking at one zero:
      - result = seen[i+1] + seen[i-1] + 1
    - If we are looking at multiple zeroes:
      - result = max(seen[i+1], seen[i-1]) + 1
    - Update max_result based on result
We should make a note that Python does not have a logical right shift operator built in. We can either use a positive number or implement one for a 32 bit number:
num % 0x100000000 >> n
Complexity:
- Time: O(b)
- Space: O(b)
Code
class Bits(object):
​
    MAX_BITS = 32
    def _build_seen_list(self, num):
        seen = []
        looking_for = 0
        count = 0
        for _ in range(self.MAX_BITS):
            if num & 1 != looking_for:
                seen.append(count)
                looking_for = not looking_for
                count = 0
            count += 1
            num >>= 1
        seen.append(count)
        return seen
    def flip_bit(self, num):
        if num is None:
            raise TypeError('num cannot be None')
        if num == -1:
            return self.MAX_BITS
        if num == 0:
            return 1
        seen = self._build_seen_list(num)
        max_result = 0
        looking_for = 0
        for index, count in enumerate(seen):
            result = 0
            # Only look for zeroes
            if looking_for == 1:
                looking_for = not looking_for
                continue
            # First iteration, take trailing zeroes
            # or trailing ones into account
            if index == 0:
                if count != 0:
                    # Trailing zeroes
                    try:
                        result = seen[index + 1] + 1
                    except IndexError:
                        result = 1
            # Last iteration
            elif index == len(seen) - 1:
                result = 1 + seen[index - 1]
            else:
                # One zero
                if count == 1:
                    result = seen[index + 1] + seen[index - 1] + 1
                # Multiple zeroes
                else:
                    result = max(seen[index + 1], seen[index - 1]) + 1
            if result > max_result:
                max_result = result
            looking_for = not looking_for
        return max_result
Unit Test
%%writefile test_flip_bit.py
from nose.tools import assert_equal, assert_raises
​
​
class TestBits(object):
​
    def test_flip_bit(self):
        bits = Bits()
        assert_raises(TypeError, bits.flip_bit, None)
        assert_equal(bits.flip_bit(0), 1)
        assert_equal(bits.flip_bit(-1), bits.MAX_BITS)
        num = int('00001111110111011110001111110000', base=2)
        expected = 10
        assert_equal(bits.flip_bit(num), expected)
        num = int('00000100111011101111100011111011', base=2)
        expected = 9
        assert_equal(bits.flip_bit(num), expected)
        num = int('00010011101110111110001111101111', base=2)
        expected = 10
        assert_equal(bits.flip_bit(num), expected)
        print('Success: test_print_binary')
​
​
def main():
    test = TestBits()
    test.test_flip_bit()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Given two 16 bit numbers, n and m, and two indices i, j, insert m into n such that m starts at bit j and ends at bit i.
A: Constraints
- Can we assume j > i?
  - Yes
- Can we assume i through j have enough space for m?
  - Yes
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None as an input -> Exception
- Negative index for i or j -> Exception
- General case
i = 2, j = 6
                    j    i
n      = 0000 0100 0011 1101
m      = 0000 0000 0001 0011
result = 0000 0100 0100 1101
Algorithm
                    j    i
n      = 0000 0100 0011 1101
m      = 0000 0000 0001 0011
lmask  = 1111 1111 1111 1111  -1
lmask  = 1111 1111 1000 0000  -1 << (j + 1)
rmask  = 0000 0000 0000 0001   1
rmask  = 0000 0000 0000 0100   1 << i
rmask  = 0000 0000 0000 0011   (1 << i) -1
mask   = 1111 1111 1000 0011   lmask | rmask
n      = 0000 0100 0011 1101
mask   = 1111 1111 1000 0011   n & mask
--------------------------------------------------
n2     = 0000 0100 0000 0001
n2     = 0000 0100 0000 0001
mask2  = 0000 0000 0100 1100   m << i
--------------------------------------------------
result = 0000 0100 0100 1101   n2 | mask2
Complexity:
- Time: O(b), where b is the number of bits
- Space: O(b), where b is the number of bits
Code
class Bits(object):
​
    def insert_m_into_n(self, m, n, i, j):
        if None in (m, n, i, j):
            raise TypeError('Argument cannot be None')
        if i < 0 or j < 0:
            raise ValueError('Index cannot be negative')
        left_mask = -1 << (j + 1)
        right_mask = (1 << i) - 1
        n_mask = left_mask | right_mask
        # Clear bits from j to i, inclusive
        n_cleared = n & n_mask
        # Shift m into place before inserting it into n
        m_mask = m << i
        return n_cleared | m_mask
Unit Test
%%writefile test_insert_m_into_n.py
from nose.tools import assert_equal
​
​
class TestBit(object):
​
    def test_insert_m_into_n(self):
        n = int('0000010000111101', base=2)
        m = int('0000000000010011', base=2)
        expected = int('0000010001001101', base=2)
        bits = Bits()
        assert_equal(bits.insert_m_into_n(m, n, i=2, j=6), expected)
        print('Success: test_insert_m_into_n')
​
​
def main():
    test = TestBit()
    test.test_insert_m_into_n()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Swap the odd and even bits of a positive integer with as few operations as possible.
A: Constraints
- Can we assume the input is always a positive int?
  - Yes
- Can we assume we're working with 32 bits?
  - Yes
- Is the output an int?
  - Yes
- Can we assume the inputs are valid (not None)?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> Exception
- 0 -> 0
- -1 -> -1
- General case input  = 0000 1001 1111 0110
- result = 0000 0110 1111 1001
Algorithm
* Isolate the odd bits with a mask:
    0000 1001 1111 0110  num
    1010 1010 1010 1010  mask
    --------------------------------
    0000 1000 1010 0010  num & mask
* Shift the odd bits right:
    0000 0100 0101 0001  odd
* Isolate the even bits with a mask:
    0000 1001 1111 0110  num
    0101 0101 0101 0101  mask
    --------------------------------
    0000 0001 0101 0100  num & mask
* Shift the even bits left:
    0000 0010 1010 1000  even
* Return even | odd
    0000 0100 0101 0001  odd
    0000 0010 1010 1000  even
    --------------------------------
    0000 0110 1111 1001  odd | even
Complexity:
- Time: O(b), where b is the number of bits
- Space: O(b), where b is the number of bits
Code
class Bits(object):
​
    def pairwise_swap(self, num):
        if num is None:
            raise TypeError('num cannot be None')
        if num == 0 or num == 1:
            return num
        odd = (num & int('1010101010101010', base=2)) >> 1
        even = (num & int('0101010101010101', base=2)) << 1
        return odd | even
Unit Test
%%writefile test_pairwise_swap.py
from nose.tools import assert_equal, assert_raises
​
​
class TestBits(object):
​
    def test_pairwise_swap(self):
        bits = Bits()
        assert_equal(bits.pairwise_swap(0), 0)
        assert_equal(bits.pairwise_swap(1), 1)
        num = int('0000100111110110', base=2)
        expected = int('0000011011111001', base=2)
        assert_equal(bits.pairwise_swap(num), expected)
        assert_raises(TypeError, bits.pairwise_swap, None)
        print('Success: test_pairwise_swap')
​
​
def main():
    test = TestBits()
    test.test_pairwise_swap()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Given a real number between 0 and 1, print the binary representation. If the length of the representation is > 32, return 'ERROR'.
A: Constraints
- Is the input a float?
  - Yes
- Is the output a string?
  - Yes
- Is 0 and 1 inclusive?
  - No
- Does the result include a trailing zero and decimal point?
  - Yes
- Is the leading zero and decimal point counted in the 32 char limit?
  - Yes
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> 'ERROR'
- Out of bounds (0, 1) -> 'ERROR'
- General case
  - 0.625 -> 0.101
  - 0.987654321 -> 'ERROR'
Algorithm
- Set the result to '0.'
- Start with a fraction of 0.5, which is 0.1 in base 2
- Loop while num > 0
  - Check num versus fraction
    - If num > fraction, add a 1 to the result, num -= fraction
    - Else, add a 0 to the result
    - If the len(result) > 32, return 'ERROR'
Complexity:
- Time: O(1)
- Space: O(1)
Code
from __future__ import division
​
​
class Bits(object):
    MAX_BITS = 32
​
    def print_binary(self, num):
        if num is None or num >= 1 or num <= 0:
            return 'ERROR'
        result = ['0', '.']
        fraction = 0.5
        while num:
            if num >= fraction:
                result.append('1')
                num -= fraction
            else:
                result.append('0')
            if len(result) > self.MAX_BITS:
                return 'ERROR'
            fraction /= 2
        return ''.join(result)
Unit Test
%%writefile test_print_binary.py
from nose.tools import assert_equal
​
​
class TestBits(object):
​
    def test_print_binary(self):
        bit = Bits()
        assert_equal(bit.print_binary(None), 'ERROR')
        assert_equal(bit.print_binary(0), 'ERROR')
        assert_equal(bit.print_binary(1), 'ERROR')
        num = 0.625
        expected = '0.101'
        assert_equal(bit.print_binary(num), expected)
        num = 0.987654321
        assert_equal(bit.print_binary(num), 'ERROR')
        print('Success: test_print_binary')
​
​
def main():
    test = TestBits()
    test.test_print_binary()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Check if a number is prime.
A: Constraints
- Is it correct that 1 is not considered a prime number?
  - Yes
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> Exception
- Not an int -> Exception
- Less than 2 -> False
- General case
Algorithm
For a number to be prime, it must be 2 or greater and cannot be divisible by another number other than itself (and 1).
We'll check by dividing all numbers from 2 to the input number to determine if the number is prime.
As an optimization, we can divide from 2 to the square root of the input number. For each value that divides the input number evenly, there is a complement b where a * b = n. If a > sqrt(n) then b < sqrt(n) because sqrt(n^2) = n.
Complexity:
- Time: O(n) where n is the value of the input number
- Space: O(1)
Sieve of Eratosthenes
The Sieve of Eratosthenes provides a more efficient way of computing and generating primes. See the challenge "Generate a list of primes" for more details.
Code
import math
​
​
class Math(object):
​
    def check_prime(self, num):
        if num is None or not isinstance(num, int):
            raise TypeError('num must be an int')
        if num < 2:
            return False
        for i in range(2, num):
            if num % i == 0:
                return False
        return True
​
    def check_prime_optimized(self, num):
        if num is None or not isinstance(num, int):
            raise TypeError('num must be an int')
        if num < 2:
            return False
        for i in range(2, int(math.sqrt(num)+1)):
            if num % i == 0:
                return False
        return True
Unit Test
%%writefile test_check_prime.py
from nose.tools import assert_equal, assert_raises
​
​
class TestMath(object):
​
    def test_check_prime(self):
        math = Math()
        assert_raises(TypeError, math.check_prime, None)
        assert_raises(TypeError, math.check_prime, 98.6)
        assert_equal(math.check_prime(0), False)
        assert_equal(math.check_prime(1), False)
        assert_equal(math.check_prime(97), True)
        print('Success: test_check_prime')
​
​
def main():
    test = TestMath()
    test.test_check_prime()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Generate a list of primes.
A: Constraints
- Is it correct that 1 is not considered a prime number?
  - Yes
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> Exception
- Not an int -> Exception
- 20 -> 2, 3, 5, 7, 11, 13, 17, 19
Algorithm
For a number to be prime, it must be 2 or greater and cannot be divisible by another number other than itself (and 1).
We'll use the Sieve of Eratosthenes. All non-prime numbers are divisible by a prime number.
- Use an array (or bit array, bit vector) to keep track of each integer up to the max
- Start at 2, end at sqrt(max)
  - We can use sqrt(max) instead of max because:
    - For each value that divides the input number evenly, there is a complement b where a * b = n
    - If a > sqrt(n) then b < sqrt(n) because sqrt(n^2) = n
  - "Cross off" all numbers divisible by 2, 3, 5, 7, ... by setting array[index] to False
Complexity:
- Time: O(n log log n)
- Space: O(n)
Wikipedia's animation:
Code
import math
​
​
class PrimeGenerator(object):
​
    def generate_primes(self, max_num):
        if max_num is None or not isinstance(max_num, int):
            raise TypeError('max_num must be an int')
        array = [True] * max_num
        array[0] = False
        array[1] = False
        prime = 2
        while prime <= math.sqrt(max_num):
            self._cross_off(array, prime)
            prime = self._next_prime(array, prime)
        return array
​
    def _cross_off(self, array, prime):
        for index in range(prime*prime, len(array), prime):
            # Start with prime*prime because if we have a k*prime
            # where k < prime, this value would have already been
            # previously crossed off
            array[index] = False
​
    def _next_prime(self, array, prime):
        next = prime + 1
        while next < len(array) and not array[next]:
            next += 1
        return next
Unit Test
%%writefile test_generate_primes.py
from nose.tools import assert_equal, assert_raises
​
​
class TestMath(object):
​
    def test_generate_primes(self):
        prime_generator = PrimeGenerator()
        assert_raises(TypeError, prime_generator.generate_primes, None)
        assert_raises(TypeError, prime_generator.generate_primes, 98.6)
        assert_equal(prime_generator.generate_primes(20), [False, False, True,
                                                           True, False, True,
                                                           False, True, False,
                                                           False, False, True,
                                                           False, True, False,
                                                           False, False, True,
                                                           False, True])
        print('Success: generate_primes')
​
​
def main():
    test = TestMath()
    test.test_generate_primes()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Determine the total number of unique ways to make n cents, given coins of denominations less than n cents.
A: Constraints
- Do the coins have to reach exactly n cents?
  - Yes
- Can we assume we have an infinite number of coins to make n cents?
  - Yes
- Do we need to report the combination(s) of coins that represent the minimum?
  - No
- Can we assume the coin denominations are given in sorted order?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- coins: None or n: None -> Exception
- coins: [] or n: 0 -> 0
- coins: [1, 2, 3], n: 5 -> 5
Algorithm
We'll use a bottom-up dynamic programming approach.
The rows (i) represent the coin values.
The columns (j) represent the totals.
  -------------------------
  | 0 | 1 | 2 | 3 | 4 | 5 |
  -------------------------
0 | 1 | 0 | 0 | 0 | 0 | 0 |
1 | 1 | 1 | 1 | 1 | 1 | 1 |
2 | 1 | 1 | 2 | 2 | 3 | 3 |
3 | 1 | 1 | 2 | 3 | 4 | 5 |
  -------------------------
Number of ways to get total n with coin[n] equals:
* Number of ways to get total n with coin[n - 1] plus
* Number of ways to get total n - coin[n]
if j == 0:
    T[i][j] = 1
if row == 0:
    T[i][j] = 0
if coins[i] >= j
    T[i][j] = T[i - 1][j] + T[i][j - coins[i]]
else:
    T[i][j] = T[i - 1][j]
The answer will be in the bottom right corner of the matrix.
Complexity:
- Time: O(i * j)
- Space: O(i * j)
Code
class CoinChanger(object):
​
    def make_change(self, coins, total):
        if coins is None or total is None:
            return None
        if not coins or total == 0:
            return 0
        coins = [0] + coins
        num_rows = len(coins)
        num_cols = total + 1
        T = [[None] * num_cols for _ in range(num_rows)]
        for i in range(num_rows):
            for j in range(num_cols):
                if i == 0:
                    T[i][j] = 0
                    continue
                if j == 0:
                    T[i][j] = 1
                    continue
                if coins[i] <= j:
                    T[i][j] = T[i - 1][j] + T[i][j - coins[i]]
                else:
                    T[i][j] = T[i - 1][j]
        return T[num_rows - 1][num_cols - 1]
Unit Test
%%writefile test_coin_change.py
from nose.tools import assert_equal
​
​
class Challenge(object):
​
    def test_coin_change(self):
        coin_changer = CoinChanger()
        assert_equal(coin_changer.make_change([1, 2], 0), 0)
        assert_equal(coin_changer.make_change([1, 2, 3], 5), 5)
        assert_equal(coin_changer.make_change([1, 5, 25, 50], 10), 3)
        print('Success: test_coin_change')
​
​
def main():
    test = Challenge()
    test.test_coin_change()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Determine the minimum number of ways to make n cents, given coins of denominations less than n cents.
A: Constraints
- Do the coins have to reach exactly n cents?
  - Yes
- Can we assume we have an infinite number of coins to make n cents?
  - Yes
- Do we need to report the combination(s) of coins that represent the minimum?
  - No
- Can we assume the coin denominations are given in sorted order?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- coins: None or n: None -> Exception
- coins: [] or n: 0 -> 0
- coins: [1, 2, 3] or [3, 2, 1], n: 5 -> 2
Algorithm
We'll use top down dynamic programming with memoization.
- Base case: If the total is 0, return 0
- If the total is already in the memo, return it
- For each coin denomination:
  - If this coin > total, continue
  - Recurse, decreasing total by the coin denomination, keeping track of the min return
- Set memo[total] to the min value + 1
- Return the memo[total]
total: 5
coins: [1,2,3]
memo key: total value: min ways
memo = {
    1: 1,
    2: 1,
    3: 1,
    4: 2,
    5: 2
}
                              5
                           1, 2, 3
                          /
                         4
                      1, 2, 3
                     /
                    3
              1,    2,    3
             /       \     \____
            2         1         0
         1, 2, 3   1, 2, 3
        /   |
       1    0
    1, 2, 3
   /
  0
Complexity:
- Time: O(t * n), where t is the total and n is the number of coin denominations
- Space: O(t) for the recursion depth
Code
import sys
​
​
class CoinChanger(object):
​
    def make_change(self, coins, total):
        if coins is None or total is None:
            raise TypeError('coins or total cannot be None')
        if not coins or total == 0:
            return 0
        cache = {}
        return self._make_change(coins, total, cache)
​
    def _make_change(self, coins, total, cache):
        if total == 0:
            return 0
        if total in cache:
            return cache[total]
        min_ways = sys.maxsize
        for coin in coins:
            if total - coin < 0:
                continue
            ways = self._make_change(coins, total - coin, cache)
            if ways < min_ways:
                min_ways = ways
        cache[total] = min_ways + 1
        return cache[total]
Unit Test
%%writefile test_coin_change_min.py
from nose.tools import assert_equal, assert_raises
​
​
class TestCoinChange(object):
​
    def test_coin_change(self):
        coin_changer = CoinChanger()
        assert_raises(TypeError, coin_changer.make_change, None, None)
        assert_equal(coin_changer.make_change([], 0), 0)
        assert_equal(coin_changer.make_change([1, 2, 3], 5), 2)
        assert_equal(coin_changer.make_change([3, 2, 1], 5), 2)
        assert_equal(coin_changer.make_change([3, 2, 1], 8), 3)
        print('Success: test_coin_change')
​
​
def main():
    test = TestCoinChange()
    test.test_coin_change()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement fibonacci recursively, dynamically, and iteratively.
A: Constraints
- Does the sequence start at 0 or 1?
  - 0
- Can we assume the inputs are valid non-negative ints?
  - Yes
- Are you looking for a recursive or iterative solution?
  - Either
- Can we assume this fits memory?
  - Yes
Test Cases
- n = 0 -> 0
- n = 1 -> 1
- n = 6 -> 8
- Fib sequence: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34...
Algorithm
Recursive:
- Fibonacci is as follows: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34...
- If n = 0 or 1, return n
- Else return fib(n-1) + fib(n+2)
Complexity:
- Time: O(2^n) if recursive or iterative, O(n) if dynamic
- Space: O(n) if recursive, O(1) if iterative, O(n) if dynamic
Code
class Math(object):
​
    def fib_iterative(self, n):
        a = 0
        b = 1
        for _ in range(n):
            a, b = b, a + b
        return a
​
    def fib_recursive(self, n):
        if n == 0 or n == 1:
            return n
        else:
            return self.fib_recursive(n-1) + self.fib_recursive(n-2)
​
    def fib_dynamic(self, n):
        cache = {}
        return self._fib_dynamic(n, cache)
​
    def _fib_dynamic(self, n, cache):
        if n == 0 or n == 1:
            return n
        if n in cache:
            return cache[n]
        cache[n] = self._fib_dynamic(n-1, cache) + self._fib_dynamic(n-2, cache)
        return cache[n]
Unit Test
%%writefile test_fibonacci.py
from nose.tools import assert_equal
​
​
class TestFib(object):
​
    def test_fib(self, func):
        result = []
        expected = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
        for i in range(len(expected)):
            result.append(func(i))
        assert_equal(result, expected)
        print('Success: test_fib')
​
​
def main():
    test = TestFib()
    math = Math()
    test.test_fib(math.fib_recursive)
    test.test_fib(math.fib_dynamic)
    test.test_fib(math.fib_iterative)
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement an algorithm to have a robot move from the upper left corner to the bottom right corner of a grid.
A: Constraints
- Are there restrictions to how the robot moves?
  - The robot can only move right and down
- Are some cells invalid (off limits)?
  - Yes
- Can we assume the starting and ending cells are valid cells?
  - Yes
- Is this a rectangular grid? i.e. the grid is not jagged?
  - Yes
- Will there always be a valid way for the robot to get to the bottom right?
  - No, return None
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
o = valid cell
x = invalid cell
   0  1  2  3
0  o  o  o  o
1  o  x  o  o
2  o  o  x  o
3  x  o  o  o
4  o  o  x  o
5  o  o  o  x
6  o  x  o  x
7  o  x  o  o
- General case
expected = [(0, 0), (1, 0), (2, 0),
            (2, 1), (3, 1), (4, 1),
            (5, 1), (5, 2), (6, 2),
            (7, 2), (7, 3)]
- No valid path, say row 7, col 2 is invalid
- None input
- Empty matrix
Algorithm
To get to row r and column c [r, c], we will need to have gone:
- Right from [r, c-1] if this is a valid cell - [Path 1]
- Down from [r-1, c] if this is a valid cell - [Path 2]
If we look at [Path 1], to get to [r, c-1], we will need to have gone:
- Right from [r, c-2] if this is a valid cell
- Down from [r-1, c-1] if this is a valid cell
Continue this process until we reach the start cell or until we find that there is no path.
Base case:
- If the input row or col are < 0, or if [row, col] is not a valid cell
  - Return False
Recursive case:
We'll memoize the solution to improve performance.
- Use the memo to see if we've already processed the current cell
- If any of the following is True, append the current cell to the path and set our result to True:
  - We are at the start cell
  - We get a True result from a recursive call on:
    - [row, col-1]
    - [row-1, col]
- Update the memo
- Return the result
Complexity:
- Time: O(row * col)
- Space: O(row * col) for the recursion depth
Code
class Grid(object):
​
    def find_path(self, matrix):
        if matrix is None or not matrix:
            return None
        cache = {}
        path = []
        if self._find_path(matrix, len(matrix) - 1,
                           len(matrix[0]) - 1, cache, path):
            return path
        else:
            return None
​
    def _find_path(self, matrix, row, col, cache, path):
        if row < 0 or col < 0 or not matrix[row][col]:
            return False
        cell = (row, col)
        if cell in cache:
            return cache[cell]
        cache[cell] = (row == 0 and col == 0 or
                       self._find_path(matrix, row, col - 1, cache, path) or
                       self._find_path(matrix, row - 1, col, cache, path))
        if cache[cell]:
            path.append(cell)
        return cache[cell]
Unit Test
%%writefile test_grid_path.py
from nose.tools import assert_equal
​
​
class TestGridPath(object):
​
    def test_grid_path(self):
        grid = Grid()
        assert_equal(grid.find_path(None), None)
        assert_equal(grid.find_path([[]]), None)
        max_rows = 8
        max_cols = 4
        matrix = [[1] * max_cols for _ in range(max_rows)]
        matrix[1][1] = 0
        matrix[2][2] = 0
        matrix[3][0] = 0
        matrix[4][2] = 0
        matrix[5][3] = 0
        matrix[6][1] = 0
        matrix[6][3] = 0
        matrix[7][1] = 0
        result = grid.find_path(matrix)
        expected = [(0, 0), (1, 0), (2, 0),
                    (2, 1), (3, 1), (4, 1),
                    (5, 1), (5, 2), (6, 2),
                    (7, 2), (7, 3)]
        assert_equal(result, expected)
        matrix[7][2] = 0
        result = grid.find_path(matrix)
        assert_equal(result, None)
        print('Success: test_grid_path')
​
​
def main():
    test = TestGridPath()
    test.test_grid_path()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement the Towers of Hanoi with 3 towers and N disks.
A: Constraints
- Can we assume we already have a stack class that can be used for this problem?
  - Yes
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None tower(s) -> Exception
- 0 disks -> None
- 1 disk
- 2 or more disks
Algorithm
- Create three stacks to represent each tower
- def hanoi(n, src, dest, buffer):
  - If 0 disks return
  - hanoi(n-1, src, buffer, dest)
  - Move remaining element from src to dest
  - hanoi(n-1, buffer, dest, src)
Complexity:
- Time: O(2^n)
- Space: O(m) where m is the number of recursion levels
Code
class Hanoi(object):
​
    def move_disks(self, num_disks, src, dest, buff):
        if src is None or dest is None or buff is None:
            raise TypeError('Cannot have a None input')
        self._move_disks(num_disks, src, dest, buff)
​
    def _move_disks(self, num_disks, src, dest, buff):
        if num_disks == 0:
            return
        self.move_disks(num_disks - 1, src, buff, dest)
        dest.push(src.pop())
        self.move_disks(num_disks - 1, buff, dest, src)
Unit Test
%%writefile test_hanoi.py
from nose.tools import assert_equal, assert_raises
​
​
class TestHanoi(object):
​
    def test_hanoi(self):
        hanoi = Hanoi()
        num_disks = 3
        src = Stack()
        buff = Stack()
        dest = Stack()
​
        print('Test: None towers')
        assert_raises(TypeError, hanoi.move_disks, num_disks, None, None, None)
​
        print('Test: 0 disks')
        hanoi.move_disks(num_disks, src, dest, buff)
        assert_equal(dest.pop(), None)
​
        print('Test: 1 disk')
        src.push(5)
        hanoi.move_disks(num_disks, src, dest, buff)
        assert_equal(dest.pop(), 5)
​
        print('Test: 2 or more disks')
        for disk_index in range(num_disks, -1, -1):
            src.push(disk_index)
        hanoi.move_disks(num_disks, src, dest, buff)
        for disk_index in range(0, num_disks):
            assert_equal(dest.pop(), disk_index)
​
        print('Success: test_hanoi')
​
​
def main():
    test = TestHanoi()
    test.test_hanoi()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find the longest increasing subsequence.
A: Constraints
- Are duplicates possible?
  - Yes
- Can we assume the inputs are integers?
  - Yes
- Can we assume the inputs are valid?
  - No
- Do we expect the result to be an array of the longest increasing subsequence?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> Exception
- [] -> []
- [3, 4, -1, 0, 6, 2, 3] -> [-1, 0, 2, 3]
Algorithm
We'll use bottom up dynamic programming to build a table.
Init a temp array of size len(input) to 1.
We'll use l and r to iterate through the input.
Array prev will hold the index of the prior smaller value, used to reconstruct the final sequence.
if input[l] < input[r]:
    if temp[r] < temp[l] + 1:
        temp[r] = temp[l] + 1
        prev[r] = l
        l  r
index:  0  1  2  3  4  5  6
---------------------------
input:  3  4 -1  0  6  2  3
temp:   1  2  1  1  1  1  1
prev:   x  x  x  x  x  x  x
End result:
index:  0  1  2  3  4  5  6
---------------------------
input:  3  4 -1  0  6  2  3
temp:   1  2  1  2  3  3  4
prev:   x  0  x  2  1  3  5
Complexity:
- Time: O(n^2)
- Space: O(n)
Code
class Subsequence(object):
​
    def longest_inc_subseq(self, seq):
        if seq is None:
            raise TypeError('seq cannot be None')
        if not seq:
            return []
        temp = [1] * len(seq)
        prev = [None] * len(seq)
        for r in range(1, len(seq)):
            for l in range(r):
                if seq[l] < seq[r]:
                    if temp[r] < temp[l] + 1:
                        temp[r] = temp[l] + 1
                        prev[r] = l
        max_val = 0
        max_index = -1
        results = []
        for index, value in enumerate(temp):
            if value > max_val:
                max_val = value
                max_index = index
        curr_index = max_index
        while curr_index is not None:
            results.append(seq[curr_index])
            curr_index = prev[curr_index]
        return results[::-1]
Unit Test
%%writefile test_longest_increasing_subseq.py
from nose.tools import assert_equal, assert_raises
​
​
class TestLongestIncreasingSubseq(object):
​
    def test_longest_increasing_subseq(self):
        subseq = Subsequence()
        assert_raises(TypeError, subseq.longest_inc_subseq, None)
        assert_equal(subseq.longest_inc_subseq([]), [])
        seq = [3, 4, -1, 0, 6, 2, 3]
        expected = [-1, 0, 2, 3]
        assert_equal(subseq.longest_inc_subseq(seq), expected)
        print('Success: test_longest_increasing_subseq')
​
​
def main():
    test = TestLongestIncreasingSubseq()
    test.test_longest_increasing_subseq()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Given two strings, find the longest common substring.
A: Constraints
- Can we assume the inputs are valid?
  - No
- Can we assume the strings are ASCII?
  - Yes
- Is this case sensitive?
  - Yes
- Is a substring a contiguous block of chars?
  - Yes
- Do we expect a string as a result?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- str0 or str1 is None -> Exception
- str0 or str1 equals 0 -> ''
- General case
str0 = 'ABCDEFGHIJ' str1 = 'FOOBCDBCDE'
result: 'BCDE'
Algorithm
We'll use bottom up dynamic programming to build a table.
The rows (i) represent str0.
The columns (j) represent str1.
                       str1
  -------------------------------------------------
  |   |   | A | B | C | D | E | F | G | H | I | J |
  -------------------------------------------------
  |   | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
  | F | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 |
  | O | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 |
s | O | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 |
t | B | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
r | C | 0 | 0 | 1 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
0 | D | 0 | 0 | 1 | 2 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
  | B | 0 | 0 | 1 | 2 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
  | C | 0 | 0 | 1 | 2 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
  | D | 0 | 0 | 1 | 2 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
  | E | 0 | 0 | 1 | 2 | 3 | 4 | 4 | 4 | 4 | 4 | 4 |
  -------------------------------------------------
if str1[j] != str0[i]:
    T[i][j] = max(
        T[i][j - 1],
        T[i - 1][j])
else:
    T[i][j] = T[i - 1][j - 1] + 1
Complexity:
- Time: O(m * n), where m is the length of str0 and n is the length of str1
- Space: O(m * n), where m is the length of str0 and n is the length of str1
Code
class StringCompare(object):
​
    def longest_common_substr(self, str0, str1):
        if str0 is None or str1 is None:
            raise TypeError('str input cannot be None')
        # Add one to number of rows and cols for the dp table's
        # first row of 0's and first col of 0's
        num_rows = len(str0) + 1
        num_cols = len(str1) + 1
        T = [[None] * num_cols for _ in range(num_rows)]
        for i in range(num_rows):
            for j in range(num_cols):
                if i == 0 or j == 0:
                    T[i][j] = 0
                elif str0[j - 1] != str1[i - 1]:
                    T[i][j] = max(T[i][j - 1],
                                  T[i - 1][j])
                else:
                    T[i][j] = T[i - 1][j - 1] + 1
        results = ''
        i = num_rows - 1
        j = num_cols - 1
        # Walk backwards to determine the substring
        while T[i][j]:
            if T[i][j] == T[i][j - 1]:
                j -= 1
            elif T[i][j] == T[i - 1][j]:
                i -= 1
            elif T[i][j] == T[i - 1][j - 1] + 1:
                results += str1[i - 1]
                i -= 1
                j -= 1
            else:
                raise Exception('Error constructing table')
        # Walking backwards results in a string in reverse order
        return results[::-1]
Unit Test
%%writefile test_longest_common_substr.py
from nose.tools import assert_equal, assert_raises
​
​
class TestLongestCommonSubstr(object):
​
    def test_longest_common_substr(self):
        str_comp = StringCompare()
        assert_raises(TypeError, str_comp.longest_common_substr, None, None)
        assert_equal(str_comp.longest_common_substr('', ''), '')
        str0 = 'ABCDEFGHIJ'
        str1 = 'FOOBCDBCDE'
        expected = 'BCDE'
        assert_equal(str_comp.longest_common_substr(str0, str1), expected)
        print('Success: test_longest_common_substr')
​
​
def main():
    test = TestLongestCommonSubstr()
    test.test_longest_common_substr()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find the magic index in an array, where array[i] = i.
A: Constraints
- Is the array sorted?
  - Yes
- Are the elements in the array distinct?
  - No
- Does a magic index always exist?
  - No
- If there is no magic index, do we just return -1?
  - Yes
- Are negative values allowed in the array?
  - Yes
- If there are multiple magic values, what do we return?
  - Return the left-most one
- Can we assume this fits memory?
  - Yes
Test Cases
- None input -> -1
- Empty array -> -1
a[i]  -4 -2  2  6  6  6  6 10
  i    0  1  2  3  4  5  6  7
Result: 2
a[i]  -4 -2  1  6  6  6  6 10
  i    0  1  2  3  4  5  6  7
Result: 6
a[i]  -4 -2  1  6  6  6  7 10
  i    0  1  2  3  4  5  6  7
Result: -1
Algorithm
We'll use a binary search to split the search space in half on each iteration. To obtain more efficiency, we can do a little better than a naive left and half split.
In the example below, we see that i == 5 cannot be the magic index, otherwise a[5] would have to equal 5 (note a[4] == 6).
a[i]  -4 -2  2  6  6  6  6 10
  i    0  1  1  3  4  5  6  7
                  mid
Similarly, in the example below we can further trim the left search space.
a[i]  -4 -2  2  2  2  6  6 10
  i    0  1  2  3  4  5  6  7
                  mid
- Calculate mid
- If mid == array[mid], return mid
- Recurse on the left side of the array
  - start: 0
  - end: min(mid-1, array[mid]
- Recurse on the right side of the array
  - start: max(mid+1, array[mid]
  - end: end
Complexity:
- Time: O(log(n))
- Space: O(log(n))
Code
from __future__ import division
​
​
class MagicIndex(object):
​
    def find_magic_index(self, array):
        if array is None or not array:
            return -1
        return self._find_magic_index(array, 0, len(array) - 1)
​
    def _find_magic_index(self, array, start, end):
        if end < start or start < 0 or end >= len(array):
            return -1
        mid = (start + end) // 2
        if mid == array[mid]:
            return mid
        left_end = min(mid - 1, array[mid])
        left_result = self._find_magic_index(array, start, end=left_end)
        if left_result != -1:
            return left_result
        right_start = max(mid + 1, array[mid])
        right_result = self._find_magic_index(array, start=right_start, end=end)
        if right_result != -1:
            return right_result
        return -1
Unit Test
%%writefile test_find_magic_index.py
from nose.tools import assert_equal
​
​
class TestFindMagicIndex(object):
​
    def test_find_magic_index(self):
        magic_index = MagicIndex()
        assert_equal(magic_index.find_magic_index(None), -1)
        assert_equal(magic_index.find_magic_index([]), -1)
        array = [-4, -2, 2, 6, 6, 6, 6, 10]
        assert_equal(magic_index.find_magic_index(array), 2)
        array = [-4, -2, 1, 6, 6, 6, 6, 10]
        assert_equal(magic_index.find_magic_index(array), 6)
        array = [-4, -2, 1, 6, 6, 6, 7, 10]
        assert_equal(magic_index.find_magic_index(array), -1)
        print('Success: test_find_magic')
​
​
def main():
    test = TestFindMagicIndex()
    test.test_find_magic_index()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Given a list of 2x2 matrices, minimize the cost of matrix multiplication.
A: Constraints
- Do we just want to calculate the cost and not list the actual order of operations?
  - Yes
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> Exception
- [] -> 0
- [Matrix(2, 3), Matrix(3, 6), Matrix(6, 4), Matrix(4, 5)] -> 124
Algorithm
We'll use bottom up dynamic programming to build a table.
  0    1    2    3
[2,3][3,6][6,4][4,5]
Case: 0 * 1
2 * 3 * 6 = 36
Case: 1 * 2
3 * 6 * 4 = 72
Case: 2 * 3
6 * 4 * 5 = 120
Case: 0 * 1 * 2
0 * (1 * 2) = 2 * 3 * 4 + 72 = 96
(0 * 1) * 2 = 36 + 2 * 6 * 4 = 84
min: 84
Case: 1 * 2 * 3
1 * (2 * 3) = 3 * 6 * 5 + 120 = 210
(1 * 2) * 3 = 72 + 3 * 4 * 5 = 132
min: 132
Case: 0 * 1 * 2 * 3
0 * (1 * 2 * 3) = 2 * 3 * 5 + 132 = 162
(0 * 1) * (2 * 3) = 36 + 120 + 2 * 6 * 5 = 216
(0 * 1 * 2) * 3 = 84 + 2 * 4 * 5 = 124
min: 124
  ---------------------
  | 0 |  1 |  2 |   3 |
  ---------------------
0 | 0 | 36 | 84 | 124 |
1 | x |  0 | 72 | 132 |
2 | x |  x |  0 | 120 |
3 | x |  x |  x |   0 |
  ---------------------
min cost = T[0][cols-1] = 124
for k in range(i, j):
    T[i][j] = minimum of (T[i][k] + T[k+1][j] +
                          m[i].first * m[k].second * m[j].second) for all k
Explanation of k
  0    1    2    3
[2,3][3,6][6,4][4,5]
Fill in the missing cell, where i = 0, j = 3
  ---------------------
  | 0 |  1 |  2 |   3 |
  ---------------------
0 | 0 | 36 | 84 | ??? |
1 | x |  0 | 72 | 132 |
2 | x |  x |  0 | 120 |
3 | x |  x |  x |   0 |
  ---------------------
Case: 0 * (1 * 2 * 3), k = 0
i = 0, j = 3
0 * (1 * 2 * 3) = 2 * 3 * 5 + 132 = 162
T[i][k] + T[k+1][j] + m[i].first * m[k].second * m[j].second
T[0][0] + T[1][3] + 2 * 3 * 5
0 + 132 + 30 = 162
Case: (0 * 1) * (2 * 3), k = 1
i = 0, j = 3
(0 * 1) * (2 * 3) = 36 + 120 + 2 * 6 * 5 = 216
T[i][k] + T[k+1][j] + m[i].first * m[k].second * m[j].second
T[0][1] + T[2][3] + 2 * 6 * 5
36 + 120 + 60 = 216
Case: (0 * 1 * 2) * 3, k = 2
i = 0, j = 3
(0 * 1 * 2) * 3 = 84 + 2 * 4 * 5 = 124
T[i][k] + T[k+1][j] + m[i].first * m[k].second * m[j].second
T[0][2] + T[3][3] + 2 * 4 * 5
84 + 0 + 40 = 124
Complexity:
- Time: O(n^3)
- Space: O(n^2)
Code
class Matrix(object):
​
    def __init__(self, first, second):
        self.first = first
        self.second = second
import sys
​
​
class MatrixMultiplicationCost(object):
​
    def find_min_cost(self, matrices):
        if matrices is None:
            raise TypeError('matrices cannot be None')
        if not matrices:
            return 0
        size = len(matrices)
        T = [[0] * size for _ in range(size)]
        for offset in range(1, size):
            for i in range(size-offset):
                j = i + offset
                min_cost = sys.maxsize
                for k in range(i, j):
                    cost = (T[i][k] + T[k+1][j] +
                            matrices[i].first *
                            matrices[k].second *
                            matrices[j].second)
                    if cost < min_cost:
                        min_cost = cost
                T[i][j] = min_cost
        return T[0][size-1]
Unit Test
%%writefile test_find_min_cost.py
from nose.tools import assert_equal, assert_raises
​
​
class TestMatrixMultiplicationCost(object):
​
    def test_find_min_cost(self):
        matrix_mult_cost = MatrixMultiplicationCost()
        assert_raises(TypeError, matrix_mult_cost.find_min_cost, None)
        assert_equal(matrix_mult_cost.find_min_cost([]), 0)
        matrices = [Matrix(2, 3),
                    Matrix(3, 6),
                    Matrix(6, 4),
                    Matrix(4, 5)]
        expected_cost = 124
        assert_equal(matrix_mult_cost.find_min_cost(matrices), expected_cost)
        print('Success: test_find_min_cost')
​
​
def main():
    test = TestMatrixMultiplicationCost()
    test.test_find_min_cost()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Given a list of stock prices on each consecutive day, determine the max profits with k transactions.
A: Constraints
- Is k the number of sell transactions?
  - Yes
- Can we assume the prices input is an array of ints?
  - Yes
- Can we assume the inputs are valid?
  - No
- If the prices are all decreasing and there is no opportunity to make a profit, do we just return 0?
  - Yes
- Should the output be the max profit and days to buy and sell?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
* Prices: None or k: None -> None
* Prices: [] or k <= 0 -> []
* Prices: [0, -1, -2, -3, -4, -5]
    * (max profit, list of transactions)
    * (0, [])
* Prices: [2, 5, 7, 1, 4, 3, 1, 3] k: 3
    * (max profit, list of transactions)
    * (10, [Type.SELL day: 7 price: 3,
            Type.BUY  day: 6 price: 1,
            Type.SELL day: 4 price: 4,
            Type.BUY  day: 3 price: 1,
            Type.SELL day: 2 price: 7,
            Type.BUY  day: 0 price: 2])
Algorithm
We'll use bottom up dynamic programming to build a table.
The rows (i) represent the prices.
The columns (j) represent the number of transactions (k).
T[i][j] = max(T[i][j - 1],
              prices[j] - price[m] + T[i - 1][m])
m = 0...j-1
      0   1   2   3   4   5   6   7
--------------------------------------
|   | 2 | 5 | 7 | 1 | 4 | 3 | 1 | 3  |
--------------------------------------
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0  |
| 1 | 0 | 3 | 5 | 5 | 5 | 5 | 5 | 5  |
| 2 | 0 | 3 | 5 | 5 | 8 | 8 | 8 | 8  |
| 3 | 0 | 3 | 5 | 5 | 8 | 8 | 8 | 10 |
--------------------------------------
Optimization:
max_diff = max(max_diff,
               T[i - 1][j - 1] - prices[j - 1])
T[i][j] = max(T[i][j - 1],
              prices[j] + max_diff)
Complexity:
- Time: O(n * k)
- Space: O(n * k)
Code
from enum import Enum  # Python 2 users: Run pip install enum34
​
​
class Type(Enum):
    SELL = 0
    BUY = 1
​
​
class Transaction(object):
​
    def __init__(self, type, day, price):
        self.type = type
        self.day = day
        self.price = price
​
    def __eq__(self, other):
        return self.type == other.type and \
            self.day == other.day and \
            self.price == other.price
​
    def __repr__(self):
        return str(self.type) + ' day: ' + \
            str(self.day) + ' price: ' + \
            str(self.price)
class StockTrader(object):
​
    def find_max_profit(self, prices, k):
        if prices is None or k is None:
            raise TypeError('prices or k cannot be None')
        if not prices or k <= 0:
            return []
        num_rows = k + 1  # 0th transaction for dp table
        num_cols = len(prices)
        T = [[None] * num_cols for _ in range(num_rows)]
        for i in range(num_rows):
            for j in range(num_cols):
                if i == 0 or j == 0:
                    T[i][j] = 0
                    continue
                max_profit = None
                for m in range(j):
                    profit = prices[j] - prices[m] + T[i - 1][m]
                    if max_profit is None or profit > max_profit:
                        max_profit = profit
                T[i][j] = max(T[i][j - 1], max_profit)
        return self._find_max_profit_transactions(T, prices)
​
    def find_max_profit_optimized(self, prices, k):
        if prices is None or k is None:
            raise TypeError('prices or k cannot be None')
        if not prices or k <= 0:
            return []
        num_rows = k + 1
        num_cols = len(prices)
        T = [[None] * num_cols for _ in range(num_rows)]
        for i in range(num_rows):
            max_diff = prices[0] * -1
            for j in range(num_cols):
                if i == 0 or j == 0:
                    T[i][j] = 0
                    continue
                max_diff = max(
                    max_diff,
                    T[i - 1][j - 1] - prices[j - 1])
                T[i][j] = max(
                    T[i][j - 1],
                    prices[j] + max_diff)
        return self._find_max_profit_transactions(T, prices)
​
    def _find_max_profit_transactions(self, T, prices):
        results = []
        i = len(T) - 1
        j = len(T[0]) - 1
        max_profit = T[i][j]
        while i != 0 and j != 0:
            if T[i][j] == T[i][j - 1]:
                j -= 1
            else:
                sell_price = prices[j]
                results.append(Transaction(Type.SELL, j, sell_price))
                profit = T[i][j] - T[i - 1][j - 1]
                i -= 1
                j -= 1
                for m in range(j + 1)[::-1]:
                    if sell_price - prices[m] == profit:
                        results.append(Transaction(Type.BUY, m, prices[m]))
                        break
        return (max_profit, results)
Unit Test
%%writefile test_max_profit.py
from nose.tools import assert_equal
from nose.tools import assert_raises
from nose.tools import assert_true
​
​
class TestMaxProfit(object):
​
    def test_max_profit(self):
        stock_trader = StockTrader()
        assert_raises(TypeError, stock_trader.find_max_profit, None, None)
        assert_equal(stock_trader.find_max_profit(prices=[], k=0), [])
        prices = [5, 4, 3, 2, 1]
        k = 3
        assert_equal(stock_trader.find_max_profit(prices, k), (0, []))
        prices = [2, 5, 7, 1, 4, 3, 1, 3]
        profit, transactions = stock_trader.find_max_profit(prices, k)
        assert_equal(profit, 10)
        assert_true(Transaction(Type.SELL,
                                day=7,
                                price=3) in transactions)
        assert_true(Transaction(Type.BUY,
                                day=6,
                                price=1) in transactions)
        assert_true(Transaction(Type.SELL,
                                day=4,
                                price=4) in transactions)
        assert_true(Transaction(Type.BUY,
                                day=3,
                                price=1) in transactions)
        assert_true(Transaction(Type.SELL,
                                day=2,
                                price=7) in transactions)
        assert_true(Transaction(Type.BUY,
                                day=0,
                                price=2) in transactions)
        print('Success: test_max_profit')
​
​
def main():
    test = TestMaxProfit()
    test.test_max_profit()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find all valid combinations of n-pairs of parentheses.
A: Constraints
- Is the input an integer representing the number of pairs?
  - Yes
- Can we assume the inputs are valid?
  - No
- Is the output a list of valid combinations?
  - Yes
- Should the output have duplicates?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
* None -> Exception
* Negative -> Exception
* 0 -> []
* 1 -> ['()']
* 2 -> ['(())', '()()']
* 3 -> ['((()))', '(()())', '(())()', '()(())', '()()()']
Algorithm
Let l and r denote the number of left and right parentheses remaining at any given point. The algorithm makes use of the following conditions applied recursively:
- Left braces can be inserted any time, as long as we do not exhaust them i.e. l > 0.
- Right braces can be inserted, as long as the number of right braces remaining is greater than the left braces remaining i.e. r > l. Violation of the aforementioned condition produces an unbalanced string of parentheses.
- If both left and right braces have been exhausted i.e. l = 0 and r = 0, then the resultant string produced is balanced.
The algorithm can be rephrased as:
- Base case: l = 0 and r = 0
  - Add the string generated to the result set
- Case 1: l > 0
  - Add a left parenthesis to the parentheses string.
  - Recurse (l - 1, r, new_string, result_set)
- Case 2: r > l
  - Add a right parenthesis to the parentheses string.
  - Recurse (l, r - 1, new_string, result_set)
Complexity:
- Time: O(4^n/n^(3/2)), see Catalan numbers - 1, 1, 2, 5, 14, 42, 132...
- Space complexity: O(n), due to the implicit call stack storing a maximum of 2n function calls)
Code
class Parentheses(object):
​
    def find_pair(self, num_pairs):
        if num_pairs is None:
            raise TypeError('num_pairs cannot be None')
        if num_pairs < 0:
            raise ValueError('num_pairs cannot be < 0')
        if not num_pairs:
            return []
        results = []
        curr_results = []
        self._find_pair(num_pairs, num_pairs, curr_results, results)
        return results
​
    def _find_pair(self, nleft, nright, curr_results, results):
        if nleft == 0 and nright == 0:
            results.append(''.join(curr_results))
        else:
            if nleft >= 0:
                self._find_pair(nleft-1, nright, curr_results+['('], results)
            if nright > nleft:
                self._find_pair(nleft, nright-1, curr_results+[')'], results)
Unit Test
%%writefile test_n_pairs_parentheses.py
from nose.tools import assert_equal, assert_raises
​
​
class TestPairParentheses(object):
​
    def test_pair_parentheses(self):
        parentheses = Parentheses()
        assert_raises(TypeError, parentheses.find_pair, None)
        assert_raises(ValueError, parentheses.find_pair, -1)
        assert_equal(parentheses.find_pair(0), [])
        assert_equal(parentheses.find_pair(1), ['()'])
        assert_equal(parentheses.find_pair(2), ['(())',
                                                '()()'])
        assert_equal(parentheses.find_pair(3), ['((()))',
                                                '(()())',
                                                '(())()',
                                                '()(())',
                                                '()()()'])
        print('Success: test_pair_parentheses')
​
​
def main():
    test = TestPairParentheses()
    test.test_pair_parentheses()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find all permutations of an input string.
A: Constraints
- Can the input have duplicates?
  - Yes
- Can the output have duplicates?
  - No
- Is the output a list of strings?
  - Yes
- Do we have to output the results in sorted order?
  - No
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
* None -> None
* '' -> ''
* 'AABC' -> ['AABC', 'AACB', 'ABAC', 'ABCA',
             'ACAB', 'ACBA', 'BAAC', 'BACA',
             'BCAA', 'CAAB', 'CABA', 'CBAA']
Algorithm
- Build a dictionary of { chars: counts } where counts is the number of times each char is found in the input
- Loop through each item in the dictionary
  - If the counts is 0, continue
  - Add the current char to the current results
  - Decrement the current char's count in the dictionary
  - If the current result is the same length as the input, add it to the results
  - Else, recurse
  - Backtrack by:
    - Incrementing the current char's acount in the dictionary
    - Removing the just added current char from the current results
Complexity:
- Time: O(n!)
- Space: O(n!) since we are storing the results in an array
Code
from collections import OrderedDict
​
​
class Permutations(object):
​
    def _build_counts_map(self, string):
        counts_map = OrderedDict()
        for char in string:
            if char in counts_map:
                counts_map[char] += 1
            else:
                counts_map[char] = 1
        return counts_map
​
    def find_permutations(self, string):
        if string is None or string == '':
            return string
        counts_map = self._build_counts_map(string)
        curr_results = []
        results = []
        self._find_permutations(counts_map, curr_results, results, len(string))
        return results
​
    def _find_permutations(self, counts_map, curr_result,
                           results, input_length):
        for char in counts_map:
            if counts_map[char] == 0:
                continue
            curr_result.append(char)
            counts_map[char] -= 1
            if len(curr_result) == input_length:
                results.append(''.join(curr_result))
            else:
                self._find_permutations(counts_map, curr_result,
                                        results, input_length)
            counts_map[char] += 1
            curr_result.pop()
Unit Test
%%writefile test_permutations.py
from nose.tools import assert_equal
​
​
class TestPermutations(object):
​
    def test_permutations(self):
        permutations = Permutations()
        assert_equal(permutations.find_permutations(None), None)
        assert_equal(permutations.find_permutations(''), '')
        string = 'AABC'
        expected = [
            'AABC', 'AACB', 'ABAC', 'ABCA',
            'ACAB', 'ACBA', 'BAAC', 'BACA',
            'BCAA', 'CAAB', 'CABA', 'CBAA'
        ]
        assert_equal(permutations.find_permutations(string), expected)
        print('Success: test_permutations')
​
​
def main():
    test = TestPermutations()
    test.test_permutations()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Return all subsets of a set.
A: Constraints
- Should the resulting subsets be unique?
  - Yes, treat 'ab' and 'bc' as the same
- Is the empty set included as a subset?
  - Yes
- Are the inputs unique?
  - No
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
* None -> None
* '' -> ['']
* 'a' -> ['a', '']
* 'ab' -> ['a', 'ab', 'b', '']
* 'abc' -> ['a', 'ab', 'abc', 'ac',
            'b', 'bc', 'c', '']
* 'aabc' -> ['a', 'aa', 'aab', 'aabc',
             'aac', 'ab', 'abc', 'ac',
             'b', 'bc', 'c', '']
Algorithm
- Build a dictionary of {chars: counts} where counts is the number of times each char is found in the input
- Loop through each item in the dictionary
  - Keep track of the current index (first item will have current index 0)
  - If the char's count is 0, continue
  - Decrement the current char's count in the dictionary
  - Add the current char to the current results
  - Add the current result to the results
  - Recurse, passing in the current index as the new starting point index
    - When we recurse, we'll check if current index < starting point index, and if so, continue
    - This avoids duplicate results such as 'ab' and 'bc'
  - Backtrack by:
    - Removing the just added current char from the current results
    - Incrementing the current char's acount in the dictionary
Complexity:
- Time: O(2^n)
- Space: O(2^n) if we are saving each result, or O(n) if we are just printing each result
We are doubling the number of operations every time we add an element to the results: O(2^n).
Note, you could also use the following method to solve this problem:
number binary  subset
0      000      {}
1      001      {c}
2      010      {b}
3      011      {b,c}
4      100      {a}
5      101      {a,c}
6      110      {a,b}
7      111      {a,b,c}
Code
from collections import OrderedDict
​
​
class Combinatoric(object):
​
    def _build_counts_map(self, string):
        counts_map = OrderedDict()
        for char in string:
            if char in counts_map:
                counts_map[char] += 1
            else:
                counts_map[char] = 1
        return counts_map
​
    def find_power_set(self, string):
        if string is None:
            return string
        if string == '':
            return ['']
        counts_map = self._build_counts_map(string)
        curr_results = []
        results = []
        self._find_power_set(counts_map, curr_results,
                             results, index=0)
        results.append('')
        return results
​
    def _find_power_set(self, counts_map, curr_result,
                        results, index):
        for curr_index, char in enumerate(counts_map):
            if curr_index < index or counts_map[char] == 0:
                continue
            curr_result.append(char)
            counts_map[char] -= 1
            results.append(''.join(curr_result))
            self._find_power_set(counts_map, curr_result,
                                 results, curr_index)
            counts_map[char] += 1
            curr_result.pop()
Unit Test
%%writefile test_power_set.py
from nose.tools import assert_equal
​
​
class TestPowerSet(object):
​
    def test_power_set(self):
        input_set = ''
        expected = ['']
        self.run_test(input_set, expected)
        input_set = 'a'
        expected = ['a', '']
        self.run_test(input_set, expected)
        input_set = 'ab'
        expected = ['a', 'ab', 'b', '']
        self.run_test(input_set, expected)
        input_set = 'abc'
        expected = ['a', 'ab', 'abc', 'ac',
                    'b', 'bc', 'c', '']
        self.run_test(input_set, expected)
        input_set = 'aabc'
        expected = ['a', 'aa', 'aab', 'aabc',
                    'aac', 'ab', 'abc', 'ac',
                    'b', 'bc', 'c', '']
        self.run_test(input_set, expected)
        print('Success: test_power_set')
​
    def run_test(self, input_set, expected):
        combinatoric = Combinatoric()
        result = combinatoric.find_power_set(input_set)
        assert_equal(result, expected)
​
​
def main():
    test = TestPowerSet()
    test.test_power_set()
​
​
if __name__ == '__main__':
    main()

Q: Problem: You are running up n steps. If you can take a single, double, or triple step, how many possible ways are there to run up to the nth step?
A: Constraints
- If n == 0, what should the result be?
  - Go with 1, but discuss different approaches
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None or negative input -> Exception
- n == 0 -> 1
- n == 1 -> 1
- n == 2 -> 2
- n == 3 -> 4
- n == 4 -> 7
- n == 10 -> 274
Algorithm
To get to step n, we will need to have gone:
- One step from n-1
- Two steps from n-2
- Three steps from n-3
If we go the one step route above, we'll be at n-1 before taking the last step. To get to step n-1, we will need to have gone:
- One step from n-1-1
- Two steps from n-1-2
- Three steps from n-1-2
Continue this process until we reach the start.
Base case:
- If n < 0: return 0
- If n == 0: return 1
Note, if we had chosen n == 0 to return 0 instead, we would need to add additional base cases. Otherwise we'd be adding multiple 0's once we hit the base cases and not get any result > 0.
Recursive case:
We'll memoize the solution to improve performance.
- Use the memo if we've already processed the current step.
- Update the memo by adding the recursive calls to step(n-1), step(n-2), step(n-3)
Complexity:
- Time: O(n), if using memoization
- Space: O(n), where n is the recursion depth
Note: The number of ways will quickly overflow the bounds of an integer.
Code
class Steps(object):
​
    def count_ways(self, num_steps):
        if num_steps is None or num_steps < 0:
            raise TypeError('num_steps cannot be None or negative')
        cache = {}
        return self._count_ways(num_steps, cache)
​
    def _count_ways(self, num_steps, cache):
        if num_steps < 0:
            return 0
        if num_steps == 0:
            return 1
        if num_steps in cache:
            return cache[num_steps]
        cache[num_steps] = (self._count_ways(num_steps-1, cache) +
                            self._count_ways(num_steps-2, cache) +
                            self._count_ways(num_steps-3, cache))
        return cache[num_steps]
Unit Test
%%writefile test_steps.py
from nose.tools import assert_equal, assert_raises
​
​
class TestSteps(object):
​
    def test_steps(self):
        steps = Steps()
        assert_raises(TypeError, steps.count_ways, None)
        assert_raises(TypeError, steps.count_ways, -1)
        assert_equal(steps.count_ways(0), 1)
        assert_equal(steps.count_ways(1), 1)
        assert_equal(steps.count_ways(2), 2)
        assert_equal(steps.count_ways(3), 4)
        assert_equal(steps.count_ways(4), 7)
        assert_equal(steps.count_ways(10), 274)
        print('Success: test_steps')
​
​
def main():
    test = TestSteps()
    test.test_steps()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Sort an array of strings so all anagrams are next to each other.
A: Constraints
- Are there any other sorting requirements other than the grouping of anagrams?
  - No
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> Exception
- [] -> []
- General case
  - Input: ['ram', 'act', 'arm', 'bat', 'cat', 'tab']
  - Result: ['arm', 'ram', 'act', 'cat', 'bat', 'tab']
Algorithm
Input: ['ram', 'act', 'arm', 'bat', 'cat', 'tab']
Sort the chars for each item:
'ram' -> 'amr'
'act' -> 'act'
'arm' -> 'amr'
'abt' -> 'bat'
'cat' -> 'act'
'abt' -> 'tab'
Use a map of sorted chars to each item to group anagrams:
{
    'amr': ['ram', 'arm'],
    'act': ['act', 'cat'],
    'abt': ['bat', 'tab']
}
Result: ['arm', 'ram', 'act', 'cat', 'bat', 'tab']
Complexity:
- Time: O(k * n), due to the modified bucket sort
- Space: O(n)
Code
from collections import OrderedDict
​
​
class Anagram(object):
​
    def group_anagrams(self, items):
        if items is None:
            raise TypeError('items cannot be None')
        if not items:
            return items
        anagram_map = OrderedDict()
        for item in items:
            # Use a tuple, which is hashable and
            # serves as the key in anagram_map
            sorted_chars = tuple(sorted(item))
            if sorted_chars in anagram_map:
                anagram_map[sorted_chars].append(item)
            else:
                anagram_map[sorted_chars] = [item]
        result = []
        for value in anagram_map.values():
            result.extend(value)
        return result
Unit Test
%%writefile test_anagrams.py
from nose.tools import assert_equal, assert_raises
​
​
class TestAnagrams(object):
​
    def test_group_anagrams(self):
        anagram = Anagram()
        assert_raises(TypeError, anagram.group_anagrams, None)
        data = ['ram', 'act', 'arm', 'bat', 'cat', 'tab']
        expected = ['ram', 'arm', 'act', 'cat', 'bat', 'tab']
        assert_equal(anagram.group_anagrams(data), expected)
​
        print('Success: test_group_anagrams')
​
​
def main():
    test = TestAnagrams()
    test.test_group_anagrams()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement insertion sort.
A: Constraints
- Is a naive solution sufficient?
  - Yes
- Are duplicates allowed?
  - Yes
- Can we assume the input is valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> Exception
- Empty input -> []
- One element -> [element]
- Two or more elements
Algorithm
Wikipedia's animation:
- For each value index 1 to n - 1
  - Compare with all elements to the left of the current value to determine new insertion point
    - Hold current value in temp variable
    - Shift elements from new insertion point right
    - Insert value in temp variable
    - Break
Complexity:
- Time: O(n^2) avarage, worst. O(1) best if input is already sorted.
- Space: O(1)
- In-place
- Stable
Insertion sort works well for very small datasets where most of the input is already sorted.
Code
class InsertionSort(object):
​
    def sort(self, data):
        if data is None:
            raise TypeError('data cannot be None')
        if len(data) < 2:
            return data
        for r in range(1, len(data)):
            for l in range(r):
                if data[r] < data[l]:
                    temp = data[r]
                    data[l+1:r+1] = data[l:r]
                    data[l] = temp
        return data
Unit Test
%%writefile test_insertion_sort.py
from nose.tools import assert_equal, assert_raises
​
​
class TestInsertionSort(object):
​
    def test_insertion_sort(self):
        insertion_sort = InsertionSort()
​
        print('None input')
        assert_raises(TypeError, insertion_sort.sort, None)
​
        print('Empty input')
        assert_equal(insertion_sort.sort([]), [])
​
        print('One element')
        assert_equal(insertion_sort.sort([5]), [5])
​
        print('Two or more elements')
        data = [5, 1, 7, 2, 6, -3, 5, 7, -1]
        assert_equal(insertion_sort.sort(data), sorted(data))
​
        print('Success: test_insertion_sort')
​
​
def main():
    test = TestInsertionSort()
    test.test_insertion_sort()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Given sorted arrays A, B, merge B into A in sorted order.
A: Constraints
- Does A have enough space for B?
  - Yes
- Can the inputs have duplicate array items?
  - Yes
- Can we assume the inputs are valid?
  - No
- Does the inputs also include the actual size of A and B?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- A or B is None -> Exception
- index of last A or B < 0 -> Exception
- A or B is empty
- General case
  - A = [1, 3, 5, 7, 9, None, None, None]
  - B = [4, 5, 6]
  - A = [1, 3, 4, 5, 5, 6, 7, 9]
Algorithm
                     i                 k
A = [1,  3,  5,  7,  9,  None,  None,  None]
             j
B = [4,  5,  6]
---
A[k] = max(A[i], B[j])
                     i                 k
A = [1,  3,  5,  7,  9,  None,  None,  9]
             j
B = [4,  5,  6]
---
A[k] = max(A[i], B[j])
                 i              k
A = [1,  3,  5,  7,  9,  None,  7,  9]
             j
B = [4,  5,  6]
---
A[k] = max(A[i], B[j])
             i           k
A = [1,  3,  5,  7,  9,  6,  7,  9]
             j
B = [4,  5,  6]
---
A[k] = max(A[i], B[j])
             i       k
A = [1,  3,  5,  7,  5,  6,  7,  9]
         j
B = [4,  5,  6]
---
A[k] = max(A[i], B[j])
         i       k
A = [1,  3,  5,  5,  5,  6,  7,  9]
         j
B = [4,  5,  6]
---
A[k] = max(A[i], B[j])
         i   k
A = [1,  3,  4,  5,  5,  6,  7,  9]
     j
B = [4,  5,  6]
---
A[k] = max(A[i], B[j])
        ik
A = [1,  3,  4,  5,  5,  6,  7,  9]
B = [4,  5,  6]
---
A = [1, 3, 4, 5, 5, 6, 7, 9]
Complexity:
- Time: O(m + n)
- Space: O(1)
Code
class Array(object):
​
    def merge_into(self, source, dest, source_end_index, dest_end_index):
        if source is None or dest is None:
            raise TypeError('source or dest cannot be None')
        if source_end_index < 0 or dest_end_index < 0:
            raise ValueError('end indices must be >= 0')
        if not source:
            return dest
        if not dest:
            return source
        source_index = source_end_index - 1
        dest_index = dest_end_index - 1
        insert_index = source_end_index + dest_end_index - 1
        while dest_index >= 0:
            if source[source_index] > dest[dest_index]:
                source[insert_index] = source[source_index]
                source_index -= 1
            else:
                source[insert_index] = dest[dest_index]
                dest_index -= 1
            insert_index -= 1
        return source
Unit Test
%%writefile test_merge_into.py
from nose.tools import assert_equal, assert_raises
​
​
class TestArray(object):
​
    def test_merge_into(self):
        array = Array()
        assert_raises(TypeError, array.merge_into, None, None, None, None)
        assert_raises(ValueError, array.merge_into, [1], [2], -1, -1)
        a = [1, 2, 3]
        assert_equal(array.merge_into(a, [], len(a), 0), [1, 2, 3])
        a = [1, 2, 3]
        assert_equal(array.merge_into(a, [], len(a), 0), [1, 2, 3])
        a = [1,  3,  5,  7,  9,  None,  None,  None]
        b = [4,  5,  6]
        expected = [1, 3, 4, 5, 5, 6, 7, 9]
        assert_equal(array.merge_into(a, b, 5, len(b)), expected)
        print('Success: test_merge_into')
​
​
def main():
    test = TestArray()
    test.test_merge_into()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement merge sort.
A: Constraints
- Is a naive solution sufficient?
  - Yes
- Are duplicates allowed?
  - Yes
- Can we assume the input is valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> Exception
- Empty input -> []
- One element -> [element]
- Two or more elements
- Left and right subarrays of different lengths
Algorithm
Wikipedia's animation:
- Recursively split array into left and right halves
- Merge split arrays
  - Using two pointers, one for each half starting at index 0
    - Add the smaller element to the result array
    - Inrement pointer where smaller element exists
  - Copy remaining elements to the result array
  - Return result array
Complexity:
- Time: O(n log(n))
- Space: O(n)
Misc:
* Not in-place
* Most implementations are stable
Merge sort can be a good choice for data sets that are too large to fit in memory, as large chunks of data can be read and written to disk.
Unlike many other sorting algorithms, merge sort is not done in-place.
See: Quicksort vs merge sort: http://stackoverflow.com/a/90477
Code
from __future__ import division
​
​
class MergeSort(object):
​
    def sort(self, data):
        if data is None:
            raise TypeError('data cannot be None')
        return self._sort(data)
​
    def _sort(self, data):
        if len(data) < 2:
            return data
        mid = len(data) // 2
        left = data[:mid]
        right = data[mid:]
        left = self._sort(left)
        right = self._sort(right)
        return self._merge(left, right)
​
    def _merge(self, left, right):
        l = 0
        r = 0
        result = []
        while l < len(left) and r < len(right):
            if left[l] < right[r]:
                result.append(left[l])
                l += 1
            else:
                result.append(right[r])
                r += 1
        # Copy remaining elements
        while l < len(left):
            result.append(left[l])
            l += 1
        while r < len(right):
            result.append(right[r])
            r += 1
        return result
Unit Test
%%writefile test_merge_sort.py
from nose.tools import assert_equal, assert_raises
​
​
class TestMergeSort(object):
​
    def test_merge_sort(self):
        merge_sort = MergeSort()
​
        print('None input')
        assert_raises(TypeError, merge_sort.sort, None)
​
        print('Empty input')
        assert_equal(merge_sort.sort([]), [])
​
        print('One element')
        assert_equal(merge_sort.sort([5]), [5])
​
        print('Two or more elements')
        data = [5, 1, 7, 2, 6, -3, 5, 7, -1]
        assert_equal(merge_sort.sort(data), sorted(data))
​
        print('Success: test_merge_sort')
​
​
def main():
    test = TestMergeSort()
    test.test_merge_sort()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Given an array of n integers, generate an int not in the input. Use a minimal amount of memory.
A: Constraints
- Are we working with non-negative ints?
  - Yes
- What is the range of the integers?
  - Discuss the approach for 4 billion integers
  - Implement for 32 integers
- Can we assume the inputs are valid?
  - No
Test Cases
- None -> Exception
- [] -> Exception
- General case
  - There is an int excluded from the input -> int
  - There isn't an int excluded from the input -> None
Algorithm
The problem states to use a minimal amount of memory. We'll use a bit vector to keep track of the inputs.
Say we are given 4 billion integers, which is 2^32 integers. The number of non-negative integers would be 2^31. With a bit vector, we'll need 4 billion bits to map each integer to a bit. Say we had only 1 GB of memory or 2^32 bytes. This would leave us with 8 billion bits.
To simplify this exercise, we'll work with an input of up to 32 ints that we'll map to a bit vector of 32 bits.
input = [0, 1, 2, 3, 4...28, 29, 31]
bytes          [         1          ]  [          2         ] [          3          ] [          4          ]
index       =  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
bit_vector  =  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1  0  1
result = 30
* Loop through each item in the input, setting bit_vector[item] = True.
* Loop through the bit_vector, return the first index where bit_vector[item] == False.
Complexity:
- Time: O(b), where b is the number of bits
- Space: O(b)
Code
from bitstring import BitArray  # Run pip install bitstring
​
​
class Bits(object):
​
    def new_int(self, array, max_size):
        if not array:
            raise TypeError('array cannot be None or empty')
        bit_vector = BitArray(max_size)
        for item in array:
            bit_vector[item] = True
        for index, item in enumerate(bit_vector):
            if not item:
                return index
        return None
Unit Test
%%writefile test_new_int.py
from nose.tools import assert_equal, assert_raises
​
​
class TestBits(object):
​
    def test_new_int(self):
        bits = Bits()
        max_size = 32
        assert_raises(TypeError, bits.new_int, None, max_size)
        assert_raises(TypeError, bits.new_int, [], max_size)
        data = [item for item in range(30)]
        data.append(31)
        assert_equal(bits.new_int(data, max_size), 30)
        data = [item for item in range(32)]
        assert_equal(bits.new_int(data, max_size), None)
        print('Success: test_find_int_excluded_from_input')
​
​
def main():
    test = TestBits()
    test.test_new_int()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement quick sort.
A: Constraints
- Is a naive solution sufficient (ie not in-place)?
  - Yes
- Are duplicates allowed?
  - Yes
- Can we assume the input is valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> Exception
- Empty input -> []
- One element -> [element]
- Two or more elements
Algorithm
Wikipedia's animation:
- Set pivot to the middle element in the data
- For each element:
  - If current element is the pivot, continue
  - If the element is less than the pivot, add to left array
  - Else, add to right array
- Recursively apply quicksort to the left array
- Recursively apply quicksort to the right array
- Merge the left array + pivot + right array
Complexity:
- Time: O(n log(n)) average, best, O(n^2) worst
- Space: O(n)
Misc:
* More sophisticated implementations are in-place, although they still take up recursion depth space
* Most implementations are not stable
See Quicksort on wikipedia:
Typically, quicksort is significantly faster in practice than other Θ(nlogn) algorithms, because its inner loop can be efficiently implemented on most architectures [presumably because it has good cache locality], and in most real-world data, it is possible to make design choices which minimize the probability of requiring quadratic time.
See: Quicksort vs merge sort: http://stackoverflow.com/a/90477
Code
from __future__ import division
​
​
class QuickSort(object):
​
    def sort(self, data):
        if data is None:
            raise TypeError('data cannot be None')
        return self._sort(data)
​
    def _sort(self, data):
        if len(data) < 2:
            return data
        equal = []
        left = []
        right = []
        pivot_index = len(data) // 2
        pivot_value = data[pivot_index]
        # Build the left and right partitions
        for item in data:
            if item == pivot_value:
                equal.append(item)
            elif item < pivot_value:
                left.append(item)
            else:
                right.append(item)
        # Recursively apply quick_sort
        left_ = self._sort(left)
        right_ = self._sort(right)
        return left_ + equal + right_
Unit Test
%%writefile test_quick_sort.py
from nose.tools import assert_equal, assert_raises
​
​
class TestQuickSort(object):
​
    def test_quick_sort(self):
        quick_sort = QuickSort()
​
        print('None input')
        assert_raises(TypeError, quick_sort.sort, None)
​
        print('Empty input')
        assert_equal(quick_sort.sort([]), [])
​
        print('One element')
        assert_equal(quick_sort.sort([5]), [5])
​
        print('Two or more elements')
        data = [5, 1, 7, 2, 6, -3, 5, 7, -1]
        assert_equal(quick_sort.sort(data), sorted(data))
​
        print('Success: test_quick_sort\n')
​
​
def main():
    test = TestQuickSort()
    test.test_quick_sort()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement radix sort.
A: Constraints
- Is the input a list?
  - Yes
- Can we assume the inputs are valid?
  - Check for None in place of an array
  - Assume array elements are ints
- Do we know the max digits to handle?
  - No
- Are the digits base 10?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> Exception
- [] -> []
- [128, 256, 164, 8, 2, 148, 212, 242, 244] -> [2, 8, 128, 148, 164, 212, 242, 244, 256]
Algorithm
Sample input: [1, 220, 122, 112]
- We'll evaluate each digit starting with the ones position
  - [1, 220, 122, 112]
    - Bucket 0: 220
    - Bucket 1: 1
    - Bucket 2: 122, 112
    - Result: [220, 1, 122, 112]
  - [220, 1, 122, 112]
    - Bucket 0: 1
    - Bucket 1: 112
    - Bucket 2: 220, 122
    - Result: [1, 112, 220, 122]
  - [1, 112, 220, 122]
    - Bucket 0: 1
    - Bucket 1: 112, 122
    - Bucket 2: 220
    - Result: [1, 112, 122, 220]
Bucketing example: 123
- Ones
  - 123 // 10^0 = 123
  - 123 % 10 = 3
- Tens
  - 123 // 10^1 = 12
  - 12 % 10 = 2
- Hundreds
  - 123 // 10^2 = 1
  - 1 % 10 = 1
Complexity:
- Time: O(k*n), where n is the number of items and k is the number of digits in the largest item
- Space: O(k+n)
Misc:
* Not in-place
* Most implementations are stable
If k (the number of digits) is less than log(n), radix sort can be faster than algorithms such as quicksort.
Code
class RadixSort(object):
​
    def sort(self, array, base=10):
        if array is None:
            raise TypeError('array cannot be None')
        if not array:
            return []
        max_element = max(array)
        max_digits = len(str(abs(max_element)))
        curr_array = array
        for digit in range(max_digits):
            buckets = [[] for _ in range(base)]
            for item in curr_array:
                buckets[(item//(base**digit))%base].append(item)
            curr_array = []
            for bucket in buckets:
                curr_array.extend(bucket)
        return curr_array
Unit Test
%%writefile test_radix_sort.py
from nose.tools import assert_equal, assert_raises
​
​
class TestRadixSort(object):
​
    def test_sort(self):
        radix_sort = RadixSort()
        assert_raises(TypeError, radix_sort.sort, None)
        assert_equal(radix_sort.sort([]), [])
        array = [128, 256, 164, 8, 2, 148, 212, 242, 244]
        expected = [2, 8, 128, 148, 164, 212, 242, 244, 256]
        assert_equal(radix_sort.sort(array), expected)
        print('Success: test_sort')
​
​
def main():
    test = TestRadixSort()
    test.test_sort()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find an element in a sorted array that has been rotated a number of times.
A: Constraints
- Is the input an array of ints?
  - Yes
- Can the input have duplicates?
  - Yes
- Do we know how many times the array was rotated?
  - No
- Was the array originally sorted in increasing or decreasing order?
  - Increasing
- For the output, do we return the index?
  - Yes
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> Exception
- [] -> None
- Not found -> None
- General case with duplicates
- General case without duplicates
Algorithm
General case without dupes
index                   0   1   2   3   4   5   6   7   8   9
input                 [ 1,  3,  5,  6,  7,  8,  9, 10, 12, 14]
input rotated 1x      [10, 12, 14,  1,  3,  5,  6,  7,  8,  9]
input rotated 2x      [ 5,  6,  7,  8,  9, 10, 12, 14,  1,  3]
input rotated 3x      [10, 12, 14,  1,  3,  5,  6,  7,  8,  9]
find 1
len = 10
mid = 10 // 2 = 5
                        s                   m               e
index                   0   1   2   3   4   5   6   7   8   9
input                 [10, 12, 14,  1,  3,  5,  6,  7,  8,  9]
input[start] > input[mid]: Left half is rotated
input[end] >= input[mid]: Right half is sorted
1 is not within input[mid+1] to input[end] on the right side, go left
                        s       m       e
index                   0   1   2   3   4   5   6   7   8   9
input                 [10, 12, 14,  1,  3,  5,  6,  7,  8,  9]
input[start] <= input[mid]: Right half is rotated
input[end] >= input[mid]: Left half is sorted
1 is not within input[left] to input[mid-1] on the left side, go right
General case with dupes
                        s                   m               e
index                   0   1   2   3   4   5   6   7   8   9
input                 [ 1,  1,  1,  1,  1,  1,  1,  1,  1,  2]
input[start] == input[mid], input[mid] != input[end], go right
input rotated 1x      [ 1,  1,  2,  1,  1,  1,  1,  1,  1,  1]
input[start] == input[mid] == input[end], search both sides
Complexity:
- Time: O(log n) if there are no duplicates, else O(n)
- Space: O(m), where m is the recursion depth
Code
class Array(object):
​
    def search_sorted_array(self, array, val):
        if array is None or val is None:
            raise TypeError('array or val cannot be None')
        if not array:
            return None
        return self._search_sorted_array(array, val, start=0, end=len(array) - 1)
​
    def _search_sorted_array(self, array, val, start, end):
        if end < start:
            return None
        mid = (start + end) // 2
        if array[mid] == val:
            return mid
        # Left side is sorted
        if array[start] < array[mid]:
            if array[start] <= val < array[mid]:
                return self._search_sorted_array(array, val, start, mid - 1)
            else:
                return self._search_sorted_array(array, val, mid + 1, end)
        # Right side is sorted
        elif array[start] > array[mid]:
            if array[mid] < val <= array[end]:
                return self._search_sorted_array(array, val, mid + 1, end)
            else:
                return self._search_sorted_array(array, val, start, mid - 1)
        # Duplicates
        else:
            if array[mid] != array[end]:
                return self._search_sorted_array(array, val, mid + 1, end)
            else:
                result = self._search_sorted_array(array, val, start, mid - 1)
                if result != None:
                    return result
                else:
                    return self._search_sorted_array(array, val, mid + 1, end)
Unit Test
%%writefile test_search_sorted_array.py
from nose.tools import assert_equal, assert_raises
​
​
class TestArray(object):
​
    def test_search_sorted_array(self):
        array = Array()
        assert_raises(TypeError, array.search_sorted_array, None)
        assert_equal(array.search_sorted_array([3, 1, 2], 0), None)
        assert_equal(array.search_sorted_array([3, 1, 2], 0), None)
        data = [10, 12, 14,  1,  3,  5,  6,  7,  8,  9]
        assert_equal(array.search_sorted_array(data, val=1), 3)
        data = [ 1,  1,  2,  1,  1,  1,  1,  1,  1,  1]
        assert_equal(array.search_sorted_array(data, val=2), 2)
        print('Success: test_search_sorted_array')
​
​
def main():
    test = TestArray()
    test.test_search_sorted_array()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Search a sorted matrix for an item.
A: Constraints
- Are items in each row sorted?
  - Yes
- Are items in each column sorted?
  - Yes
- Is the sorting in ascending or descending order?
  - Ascending
- Is the matrix a rectangle? Not jagged?
  - Yes
- Is the matrix square?
  - Not necessarily
- Is the output a tuple (row, col)?
  - Yes
- Is the item you are searching for always in the matrix?
  - No
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> Exception
- General case
  - Item found -> (row, col)
  - Item not found -> None
Algorithm
Find 60 (val = 60)
 20  40  63   80
 30  50  80   90
 40  60  100 110
 50  65  105 150
* If the start of a col > val, look left
* If the end of a col < val, look right
* If the start of row > val, look up
* If the end of a row < val, look down
If we start at the upper right corner, we just need to use these cases:
* If the start of a col > val, look left
* If the end of a row < val, look down
Complexity:
- Time: O(n + m), where n and m are the matrix dimensions
- Space: O(1)
Code
class SortedMatrix(object):
​
    def find_val(self, matrix, val):
        if matrix is None or val is None:
            raise TypeError('matrix and val cannot be None')
        row = 0
        col = len(matrix[0]) - 1
        while row < len(matrix) and col >= 0:
            if matrix[row][col] == val:
                return (row, col)
            elif matrix[row][col] > val:
                col -= 1
            else:
                row += 1
        return None
Unit Test
%%writefile test_search_sorted_matrix.py
from nose.tools import assert_equal, assert_raises
​
​
class TestSortedMatrix(object):
​
    def test_find_val(self):
        matrix = [[20, 40, 63, 80],
                  [30, 50, 80, 90],
                  [40, 60, 110, 110],
                  [50, 65, 105, 150]]
        sorted_matrix = SortedMatrix()
        assert_raises(TypeError, sorted_matrix.find_val, None, None)
        assert_equal(sorted_matrix.find_val(matrix, 1000), None)
        assert_equal(sorted_matrix.find_val(matrix, 60), (2, 1))
        print('Success: test_find_val')
​
​
def main():
    test = TestSortedMatrix()
    test.test_find_val()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement selection sort.
A: Constraints
- Is a naiive solution sufficient (ie not stable, not based on a heap)?
  - Yes
- Are duplicates allowed?
  - Yes
- Can we assume the input is valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> Exception
- [] -> []
- One element -> [element]
- Two or more elements
Algorithm
Wikipedia's animation:
We can do this recursively or iteratively. Iteratively will be more efficient as it doesn't require the extra space overhead with the recursive calls.
- For each element
  - Check every element to the right to find the min
  - If min < current element, swap
Complexity:
- Time: O(n^2) average, worst, best
- Space: O(1) iterative, O(m) recursive where m is the recursion depth (unless tail-call elimination is available, then O(1)), generally not stable
- Note: Tail call elimination is not inherently available in Python. See the following StackOverflow post.
- In-place
- Most implementations are generally not stable
Selection sort might be a good option if moving elements is more expensive than comparing them, as it requires at most n-1 swaps.
The finding of a minimum element can be done with a min heap, which would change the worst-case run time to O(n log(n)) and increase the space to O(n). This is called a heap sort.
Code
class SelectionSort(object):
​
    def sort(self, data):
        if data is None:
            raise TypeError('data cannot be None')
        if len(data) < 2:
            return data
        for i in range(len(data) - 1):
            min_index = i
            for j in range(i + 1, len(data)):
                if data[j] < data[min_index]:
                    min_index = j
            if data[min_index] < data[i]:
                data[i], data[min_index] = data[min_index], data[i]
        return data
​
    def sort_iterative_alt(self, data):
        if data is None:
            raise TypeError('data cannot be None')
        if len(data) < 2:
            return data
        for i in range(len(data) - 1):
            self._swap(data, i, self._find_min_index(data, i))
        return data
​
    def sort_recursive(self, data, start):
        if data is None:
            raise TypeError('data cannot be None')
        if len(data) < 2:
            return data
        return self._sort_recursive(data, start=0)
​
    def _sort_recursive(self, data, start):
        if data is None:
            return
        if start < len(data) - 1:
            swap(data, start, self._find_min_index(data, start))
            self._sort_recursive(data, start + 1)
        return data
​
    def _find_min_index(self, data, start):
        min_index = start
        for i in range(start + 1, len(data)):
            if data[i] < data[min_index]:
                min_index = i
        return min_index
​
    def _swap(self, data, i, j):
        if i != j:
            data[i], data[j] = data[j], data[i]
        return data
Unit Test
%%writefile test_selection_sort.py
from nose.tools import assert_equal, assert_raises
​
​
class TestSelectionSort(object):
​
    def test_selection_sort(self, func):
        print('None input')
        assert_raises(TypeError, func, None)
​
        print('Empty input')
        assert_equal(func([]), [])
​
        print('One element')
        assert_equal(func([5]), [5])
​
        print('Two or more elements')
        data = [5, 1, 7, 2, 6, -3, 5, 7, -10]
        assert_equal(func(data), sorted(data))
​
        print('Success: test_selection_sort\n')
​
​
def main():
    test = TestSelectionSort()
    selection_sort = SelectionSort()
    test.test_selection_sort(selection_sort.sort)
    try:
        test.test_selection_sort(selection_sort.sort_recursive)
        test.test_selection_sort(selection_sort.sor_iterative_alt)
    except NameError:
        # Alternate solutions are only defined
        # in the solutions file
        pass
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement Fizz Buzz.
A: Constraints
- What is fizz buzz?
  - Return the string representation of numbers from 1 to n
    - Multiples of 3 -> 'Fizz'
    - Multiples of 5 -> 'Buzz'
    - Multiples of 3 and 5 -> 'FizzBuzz'
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
* None -> Exception
* < 1 -> Exception
* 15 ->
[
    '1',
    '2',
    'Fizz',
    '4',
    'Buzz',
    'Fizz',
    '7',
    '8',
    'Fizz',
    'Buzz',
    '11',
    'Fizz',
    '13',
    '14',
    'FizzBuzz'
]
Algorithm
There is no fancy algorithm to solve fizz buzz.
- Iterate from 1 through n
- Use the mod operator to determine if the current iteration is divisible by:
  - 3 and 5 -> 'FizzBuzz'
  - 3 -> 'Fizz'
  - 5 -> 'Buzz'
  - else -> string of current iteration
- return the results
Complexity:
- Time: O(n)
- Space: O(n)
Code
class Solution(object):
​
    def fizz_buzz(self, num):
        if num is None:
            raise TypeError('num cannot be None')
        if num < 1:
            raise ValueError('num cannot be less than one')
        results = []
        for i in range(1, num + 1):
            if i % 3 == 0 and i % 5 == 0:
                results.append('FizzBuzz')
            elif i % 3 == 0:
                results.append('Fizz')
            elif i % 5 == 0:
                results.append('Buzz')
            else:
                results.append(str(i))
        return results
Unit Test
%%writefile test_fizz_buzz.py
from nose.tools import assert_equal, assert_raises
​
​
class TestFizzBuzz(object):
​
    def test_fizz_buzz(self):
        solution = Solution()
        assert_raises(TypeError, solution.fizz_buzz, None)
        assert_raises(ValueError, solution.fizz_buzz, 0)
        expected = [
            '1',
            '2',
            'Fizz',
            '4',
            'Buzz',
            'Fizz',
            '7',
            '8',
            'Fizz',
            'Buzz',
            '11',
            'Fizz',
            '13',
            '14',
            'FizzBuzz'
        ]
        assert_equal(solution.fizz_buzz(15), expected)
        print('Success: test_fizz_buzz')
​
​
def main():
    test = TestFizzBuzz()
    test.test_fizz_buzz()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Create a class with an insert method to insert an int to a list. It should also support calculating the max, min, mean, and mode in O(1).
A: Constraints
- Can we assume the inputs are valid?
  - No
- Is there a range of inputs?
  - 0 <= item <= 100
- Should mean return a float?
  - Yes
- Should the other results return an int?
  - Yes
- If there are multiple modes, what do we return?
  - Any of the modes
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> TypeError
- [] -> ValueError
- [5, 2, 7, 9, 9, 2, 9, 4, 3, 3, 2]
  - max: 9
  - min: 2
  - mean: 55
  - mode: 9 or 2
Algorithm
- We'll init our max and min to None. Alternatively, we can init them to -sys.maxsize and sys.maxsize, respectively.
- For mean, we'll keep track of the number of items we have inserted so far, as well as the running sum.
- For mode, we'll keep track of the current mode and an array with the size of the given upper limit
  - Each element in the array will be init to 0
  - Each time we insert, we'll increment the element corresponding to the inserted item's value
- On each insert:
  - Update the max and min
  - Update the mean by calculating running_sum / num_items
  - Update the mode by comparing the mode array's value with the current mode
Complexity:
- Time: O(1)
- Space: O(1), we are treating the 101 element array as a constant O(1), we could also see this as O(k)
Code
from __future__ import division
​
​
class Solution(object):
​
    def __init__(self, upper_limit=100):
        self.max = None
        self.min = None
        # Mean
        self.num_items = 0
        self.running_sum = 0
        self.mean = None
        # Mode
        self.array = [0] * (upper_limit + 1)
        self.mode_ocurrences = 0
        self.mode = None
​
    def insert(self, val):
        if val is None:
            raise TypeError('val cannot be None')
        if self.max is None or val > self.max:
            self.max = val
        if self.min is None or val < self.min:
            self.min = val
        # Calculate the mean
        self.num_items += 1
        self.running_sum += val
        self.mean = self.running_sum / self.num_items
        # Calculate the mode
        self.array[val] += 1
        if self.array[val] > self.mode_ocurrences:
            self.mode_ocurrences = self.array[val]
            self.mode = val
Unit Test
%%writefile test_math_ops.py
from nose.tools import assert_equal, assert_true, assert_raises
​
​
class TestMathOps(object):
​
    def test_math_ops(self):
        solution = Solution()
        assert_raises(TypeError, solution.insert, None)
        solution.insert(5)
        solution.insert(2)
        solution.insert(7)
        solution.insert(9)
        solution.insert(9)
        solution.insert(2)
        solution.insert(9)
        solution.insert(4)
        solution.insert(3)
        solution.insert(3)
        solution.insert(2)
        assert_equal(solution.max, 9)
        assert_equal(solution.min, 2)
        assert_equal(solution.mean, 5)
        assert_true(solution.mode in (2, 9))
        print('Success: test_math_ops')
​
​
def main():
    test = TestMathOps()
    test.test_math_ops()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Given a list of stock prices, find the max profit from 1 buy and 1 sell.
A: Constraints
- Are all prices positive ints?
  - Yes
- Is the output an int?
  - Yes
- If profit is negative, do we return the smallest negative loss?
  - Yes
- If there are less than two prices, what do we return?
  - Exception
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> TypeError
- Zero or one price -> ValueError
- No profit
  - [8, 5, 3, 2, 1] -> -1
- General case
  - [5, 3, 7, 4, 2, 6, 9] -> 7
Algorithm
We'll use a greedy approach and iterate through the prices once.
- Loop through the prices
  - Update current profit (price = min_price)
  - Update the min price
  - Update the max profit
- Return max profit
Complexity:
- Time: O(n)
- Space: O(1)
Code
import sys
​
​
class Solution(object):
​
    def find_max_profit(self, prices):
        if prices is None:
            raise TypeError('prices cannot be None')
        if len(prices) < 2:
            raise ValueError('prices must have at least two values')
        min_price = prices[0]
        max_profit = -sys.maxsize
        for index, price in enumerate(prices):
            if index == 0:
                continue
            profit = price - min_price
            min_price = min(price, min_price)
            max_profit = max(profit, max_profit)
        return max_profit
Unit Test
%%writefile test_max_profit.py
from nose.tools import assert_equal, assert_raises
​
​
class TestMaxProfit(object):
​
    def test_max_profit(self):
        solution = Solution()
        assert_raises(TypeError, solution.find_max_profit, None)
        assert_raises(ValueError, solution.find_max_profit, [])
        assert_equal(solution.find_max_profit([8, 5, 3, 2, 1]), -1)
        assert_equal(solution.find_max_profit([5, 3, 7, 4, 2, 6, 9]), 7)
        print('Success: test_max_profit')
​
​
def main():
    test = TestMaxProfit()
    test.test_max_profit()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Given a list of tuples representing ranges, condense the ranges.
Example: [(2, 3), (3, 5), (7, 9), (8, 10)] -> [(2, 5), (7, 10)]
A: Constraints
- Are the tuples in sorted order?
  - No
- Are the tuples ints?
  - Yes
- Will all tuples have the first element less than the second?
  - Yes
- Is there an upper bound on the input range?
  - No
- Is the output a list of tuples?
  - Yes
- Is the output a new array?
  - Yes
- Can we assume the inputs are valid?
  - No, check for None
- Can we assume this fits memory?
  - Yes
Test Cases
* None input -> TypeError
* [] - []
* [(2, 3), (7, 9)] -> [(2, 3), (7, 9)]
* [(2, 3), (3, 5), (7, 9), (8, 10)] -> [(2, 5), (7, 10)]
* [(2, 3), (3, 5), (7, 9), (8, 10), (1, 11)] -> [(1, 11)]
* [(2, 3), (3, 8), (7, 9), (8, 10)] -> [(2, 10)]
Algorithm
- Sort the tuples based on start time
- Check each adjacent tuple to see if they can be merged
Case: * [(2, 3), (3, 8), (7, 9), (8, 10)] -> [(2, 10)]
* Sort by start time (already sorted)
* Add the first tuple to the merged_array
* Loop through each item in sorted_array starting at index 1
    * If there is no overlap
        * Add the current item to merged_array
    * Else
        * Update the last item in merged_array
            * The end time will be the max of merged_array[-1][1] and sorted_array[i][1]
Start:
                           i
                   0       1       2       3
sorted_array = [(2, 3), (3, 8), (7, 9), (8, 10)]
merged_array = [(2, 3)]
Overlap with (2, 3), (3, 8):
                           i
                   0       1       2       3
sorted_array = [(2, 3), (3, 8), (7, 9), (8, 10)]
merged_array = [(2, 8)]
Overlap with (2, 8), (7, 9):
                                   i
                   0       1       2       3
sorted_array = [(2, 3), (3, 8), (7, 9), (8, 10)]
merged_array = [(2, 9)]
Overlap with (2, 9) (8, 10):
                                   i
                   0       1       2       3
sorted_array = [(2, 3), (3, 8), (7, 9), (8, 10)]
merged_array = [(2, 10)]
Complexity:
- Time: O(n log(n))
- Space: O(n)
Code
class Solution(object):
​
    def merge_ranges(self, array):
        if array is None:
            raise TypeError('array cannot be None')
        if not array:
            return array
        sorted_array = sorted(array)
        merged_array = [sorted_array[0]]
        for index, item in enumerate(sorted_array):
            if index == 0:
                continue
            start_prev, end_prev = merged_array[-1]
            start_curr, end_curr = item
            if end_prev < start_curr:
                # No overlap, add the entry
                merged_array.append(item)
            else:
                # Overlap, update the previous entry's end value
                merged_array[-1] = (start_prev, max(end_prev, end_curr))
        return merged_array
Unit Test
%%writefile test_merge_ranges.py
from nose.tools import assert_equal, assert_raises
​
​
class TestMergeRanges(object):
​
    def test_merge_ranges(self):
        solution = Solution()
        assert_raises(TypeError, solution.merge_ranges, None)
        assert_equal(solution.merge_ranges([]), [])
        array = [(2, 3), (7, 9)]
        expected = [(2, 3), (7, 9)]
        assert_equal(solution.merge_ranges(array), expected)
        array = [(2, 3), (3, 5), (7, 9), (8, 10)]
        expected = [(2, 5), (7, 10)]
        assert_equal(solution.merge_ranges(array), expected)
        array = [(2, 3), (3, 5), (7, 9), (8, 10), (1, 11)]
        expected = [(1, 11)]
        assert_equal(solution.merge_ranges(array), expected)
        print('Success: test_merge_ranges')
​
​
def main():
    test = TestMergeRanges()
    test.test_merge_ranges()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Given a list of ints, find the products of every other int for each index.
A: Constraints
- Can we use division?
  - No
- Is the output a list of ints?
  - Yes
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
* None -> TypeError
* [] -> []
* [0] -> []
* [0, 1] -> [1, 0]
* [0, 1, 2] -> [2, 0, 0]
* [1, 2, 3, 4] -> [24, 12, 8, 6]
Algorithm
Brute force:
sum = 1
 |
[1, 2, 3, 4]
 ^
skip if both pointers are pointing to the same spot
    |
[1, 2, 3, 4]
 ^
sum *= 2
       |
[1, 2, 3, 4]
 ^
sum *= 3
          |
[1, 2, 3, 4]
 ^
sum *= 4
results.append(sum)
results = [24]
repeat for every element in the input list to obtain:
[24, 12, 8, 6]
Complexity:
- Time: O(n^2)
- Space: O(n)
Greedy
input  = [1, 2, 3, 4]
result = [2*3*4, 1*3*4, 1*2*4, 1*2*3]
Note we are duplicating multiplications with the brute force approach.
We'll calculate all products before an index, and all products after an index.
We'll then multiple these two together to form the result.
input  = [1,         2,     3,     4]
before = [1,         1,   1*2, 1*2*3]
after  = [2*3*4, 1*3*4, 1*2*4,     1]
result = [   24,    12,     8,     6]
Complexity:
- Time: O(n)
- Space: O(n)
Code
class Solution(object):
​
    def mult_other_numbers_brute(self, array):
        if array is None:
            raise TypeError('array cannot be None')
        if not array:
            return array
        if len(array) == 1:
            return []
        result = []
        for i in range(len(array)):
            curr_sum = 1
            for j in range(len(array)):
                if i == j:
                    continue
                curr_sum *= array[j]
            result.append(curr_sum)
        return result
​
    def mult_other_numbers(self, array):
        if array is None:
            raise TypeError('array cannot be None')
        if not array:
            return array
        if len(array) == 1:
            return []
        result = [None] * len(array)
        curr_product = 1
        for i in range(len(array)):
            result[i] = curr_product
            curr_product *= array[i]
        curr_product = 1
        for i in range(len(array))[::-1]:
            result[i] *= curr_product
            curr_product *= array[i]
        return result
Unit Test
%%writefile test_prod_three.py
from nose.tools import assert_equal, assert_raises
​
​
class TestProdThree(object):
​
    def test_prod_three(self):
        solution = Solution()
        assert_raises(TypeError, solution.max_prod_three, None)
        assert_raises(ValueError, solution.max_prod_three, [1, 2])
        assert_equal(solution.max_prod_three([5, -2, 3]), -30)
        assert_equal(solution.max_prod_three([5, -2, 3, 1, -1, 4]), 60)
        print('Success: test_prod_three')
​
​
def main():
    test = TestProdThree()
    test.test_prod_three()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find the highest product of three numbers in a list.
A: Constraints
- Is the input a list of integers?
  - Yes
- Can we get negative inputs?
  - Yes
- Can there be duplicate entires in the input?
  - Yes
- Will there always be at least three integers?
  - No
- Can we assume the inputs are valid?
  - No, check for None input
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> TypeError
- Less than three ints -> ValueError
- [5, -2, 3] -> -30
- [5, -2, 3, 1, -1, 4] -> 60
Algorithm
Brute force:
Use three loops and multiple each numbers.
Complexity:
- Time: O(n^3)
- Space: O(1)
Sorting:
Sort the list, multiply the last three elements.
Complexity:
- Time: O(n log(n))
- Space: O(1)
Greedy:
 0   1  2  3   4  5
[5, -2, 3, 1, -1, 4] -> 60
max_prod_of_three = -30
max_prod_of_two = -10
max_num = 5
min_prod_of_two = -10
min_num = -2
 0   1  2  3   4  5
[5, -2, 3, 1, -1, 4] -> 60
        ^
max_prod_of_three = -30
max_prod_of_two = 15
max_num = 5
min_prod_of_two = -10
min_num = -2
 0   1  2  3   4  5
[5, -2, 3, 1, -1, 4] -> 60
           ^
max_prod_of_three = 15
max_prod_of_two = 15
max_num = 5
min_prod_of_two = -10
min_num = -2
 0   1  2  3   4  5
[5, -2, 3, 1, -1, 4] -> 60
               ^
max_prod_of_three = 15
max_prod_of_two = 15
max_num = 5
min_prod_of_two = -10
min_num = -2
 0   1  2  3   4  5
[5, -2, 3, 1, -1, 4] -> 60
                  ^
max_prod_of_three = 60
max_prod_of_two = 15
max_num = 5
min_prod_of_two = -10
min_num = -2
Complexity:
- Time: O(n)
- Space: O(1)
Code
class Solution(object):
​
    def max_prod_three_nlogn(self, array):
        if array is None:
            raise TypeError('array cannot be None')
        if len(array) < 3:
            raise ValueError('array must have 3 or more ints')
        array.sort()
        product = 1
        for item in array[-3:]:
            product *= item
        return product
​
    def max_prod_three(self, array):
        if array is None:
            raise TypeError('array cannot be None')
        if len(array) < 3:
            raise ValueError('array must have 3 or more ints')
        curr_max_prod_three = array[0] * array[1] * array[2]
        max_prod_two = array[0] * array[1]
        min_prod_two = array[0] * array[1]
        max_num = max(array[0], array[1])
        min_num = min(array[0], array[1])
        for i in range(2, len(array)):
            curr_max_prod_three = max(curr_max_prod_three,
                                      max_prod_two * array[i],
                                      min_prod_two * array[i])
            max_prod_two = max(max_prod_two,
                               max_num * array[i],
                               min_num * array[i])
            min_prod_two = min(min_prod_two,
                               max_num * array[i],
                               min_num * array[i])
            max_num = max(max_num, array[i])
            min_num = min(min_num, array[i])
        return curr_max_prod_three
Unit Test
%%writefile test_prod_three.py
from nose.tools import assert_equal, assert_raises
​
​
class TestProdThree(object):
​
    def test_prod_three(self):
        solution = Solution()
        assert_raises(TypeError, solution.max_prod_three, None)
        assert_raises(ValueError, solution.max_prod_three, [1, 2])
        assert_equal(solution.max_prod_three([5, -2, 3]), -30)
        assert_equal(solution.max_prod_three([5, -2, 3, 1, -1, 4]), 60)
        print('Success: test_prod_three')
​
​
def main():
    test = TestProdThree()
    test.test_prod_three()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Given a positive integer, get the next largest number and the next smallest number with the same number of 1's as the given number.
A: Constraints
- Is the output a positive int?
  - Yes
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> Exception
- 0 -> Exception
- negative int -> Exception
- General case:
- * Input:         0000 0000 1101 0111
- * Next largest:  0000 0000 1101 1011
- * Next smallest: 0000 0000 1100 1111
Algorithm
get_next_largest
- Find the right-most non trailing zero, call this index
  - We'll use a mask of 1 and do a logical right shift on a copy of num to examine each bit starting from the right
  - Count the number of zeroes to the right of index
    - While num & 1 == 0 and num_copy != 0:
      - Increment number of zeroes
      - Logical shift right num_copy
  - Count the number of ones to the right of index
    - While num & 1 == 1 and num_copy != 0:
      - Increment number of ones
      - Logical shift right num_copy
  - The index will be the sum of number of ones and number of zeroes
  - Set the index bit
  - Clear all bits to the right of index
  - Set bits starting from 0
    - Only set (number of ones - 1) bits because we set index to 1
We should make a note that Python does not have a logical right shift operator built in. We can either use a positive number of implement one for a 32 bit number:
num % 0x100000000 >> n
get_next_smallest
- The algorithm for finding the next smallest number is very similar to finding the next largest number
  - Instead of finding the right-most non-trailing zero, we'll find the right most non-trailing one and clear it
  - Clear all bits to the right of index
  - Set bits starting at 0 to the number of ones to the right of index, plus one
Complexity:
- Time: O(b), where b is the number of bits in num
- Space: O(b), where b is the number of bits in num
Code
class Bits(object):
​
    def get_next_largest(self, num):
        if num is None:
            raise TypeError('num cannot be None')
        if num <= 0:
            raise ValueError('num cannot be 0 or negative')
        num_ones = 0
        num_zeroes = 0
        num_copy = num
        # We'll look for index, which is the right-most non-trailing zero
        # Count number of zeroes to the right of index
        while num_copy != 0 and num_copy & 1 == 0:
            num_zeroes += 1
            num_copy >>= 1
        # Count number of ones to the right of index
        while num_copy != 0 and num_copy & 1 == 1:
            num_ones += 1
            num_copy >>= 1
        # Determine index and set the bit
        index = num_zeroes + num_ones
        num |= 1 << index
        # Clear all bits to the right of index
        num &= ~((1 << index) - 1)
        # Set bits starting from 0
        num |= ((1 << num_ones - 1) - 1)
        return num
​
    def get_next_smallest(self, num):
        if num is None:
            raise TypeError('num cannot be None')
        if num <= 0:
            raise ValueError('num cannot be 0 or negative')
        num_ones = 0
        num_zeroes = 0
        num_copy = num
        # We'll look for index, which is the right-most non-trailing one
        # Count number of zeroes to the right of index
        while num_copy != 0 and num_copy & 1 == 1:
            num_ones += 1
            num_copy >>= 1
        # Count number of zeroes to the right of index
        while num_copy != 0 and num_copy & 1 == 0:
            num_zeroes += 1
            num_copy >>= 1
        # Determine index and clear the bit
        index = num_zeroes + num_ones
        num &= ~(1 << index)
        # Clear all bits to the right of index
        num &= ~((1 << index) - 1)
        # Set bits starting from 0
        num |= (1 << num_ones + 1) - 1
        return num
Unit Test
%%writefile test_get_next_largest.py
from nose.tools import assert_equal, assert_raises
​
​
class TestBits(object):
​
    def test_get_next_largest(self):
        bits = Bits()
        assert_raises(Exception, bits.get_next_largest, None)
        assert_raises(Exception, bits.get_next_largest, 0)
        assert_raises(Exception, bits.get_next_largest, -1)
        num = int('011010111', base=2)
        expected = int('011011011', base=2)
        assert_equal(bits.get_next_largest(num), expected)
        print('Success: test_get_next_largest')
​
    def test_get_next_smallest(self):
        bits = Bits()
        assert_raises(Exception, bits.get_next_smallest, None)
        assert_raises(Exception, bits.get_next_smallest, 0)
        assert_raises(Exception, bits.get_next_smallest, -1)
        num = int('011010111', base=2)
        expected = int('011001111', base=2)
        assert_equal(bits.get_next_smallest(num), expected)
        print('Success: test_get_next_smallest')
​
def main():
    test = TestBits()
    test.test_get_next_largest()
    test.test_get_next_smallest()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Assign Cookies.
See the LeetCode problem page.
Assume you are an awesome parent and want to give your children some cookies. But, you should give each child at most one cookie. Each child i has a greed factor gi, which is the minimum size of a cookie that the child will be content with; and each cookie j has a size sj. If sj >= gi, we can assign the cookie j to the child i, and the child i will be content. Your goal is to maximize the number of your content children and output the maximum number.
Note: You may assume the greed factor is always positive. You cannot assign more than one cookie to one child.
Example 1: Input: [1,2,3], [1,1]
Output: 1
Explanation: You have 3 children and 2 cookies. The greed factors of 3 children are 1, 2, 3. And even though you have 2 cookies, since their size is both 1, you could only make the child whose greed factor is 1 content. You need to output 1. Example 2: Input: [1,2], [1,2,3]
Output: 2
Explanation: You have 2 children and 3 cookies. The greed factors of 2 children are 1, 2. You have 3 cookies and their sizes are big enough to gratify all of the children, You need to output 2.
A: Constraints
- Are the inputs two list(int), one for greed factor and the other for cookie size?
  - Yes
- Are the inputs are sorted increasing order?
  - No
- Can we change inputs themselves, or do we need to make a copy?
  - You can change them
- Is the output an int?
  - Yes
- Is the greed factor always >= 1?
  - Yes
- Can we assume the inputs are valid?
  - No, check for None
- Can we assume this fits memory?
  - Yes
Test Cases
* None input -> TypeError
[1, 2, 3], [1, 1] -> 1
[1, 2], [1, 2, 3] -> 2
[7, 8, 9, 10], [5, 6, 7, 8] -> 2
Algorithm
- Sort the inputs
- We'll keep an index to the current greed factor
- For each cookie
  - Assign it to a child if its size >= the child's greed factor
    - Increment result counter
    - Increment the index to the greed factor
      - Careful of this index going out of bounds
- Return the result counter
Complexity:
- Time: O(n log n) for the sort
- Space: O(1), assuming the sort is in-place
Code
class Solution(object):
​
    def find_content_children(self, greed_indices, cookie_sizes):
        if greed_indices is None or cookie_sizes is None:
            raise TypeError('greed_indices or cookie_sizes cannot be None')
        if not greed_indices or not cookie_sizes:
            return 0
        greed_indices.sort()
        cookie_sizes.sort()
        greed_index = 0
        num_children = 0
        for size in cookie_sizes:
            if greed_index >= len(greed_indices):
                break
            if size >= greed_indices[greed_index]:
                num_children += 1
                greed_index += 1
        return num_children
Unit Test
%%writefile test_assign_cookie.py
from nose.tools import assert_equal, assert_raises
​
​
class TestAssignCookie(object):
​
    def test_assign_cookie(self):
        solution = Solution()
        assert_raises(TypeError, solution.find_content_children, None, None)
        assert_equal(solution.find_content_children([1, 2, 3],
                                                    [1, 1]), 1)
        assert_equal(solution.find_content_children([1, 2],
                                                    [1, 2, 3]), 2)
        assert_equal(solution.find_content_children([7, 8, 9, 10],
                                                    [5, 6, 7, 8]), 2)
        print('Success: test_find_content_children')
​
​
def main():
    test = TestAssignCookie()
    test.test_assign_cookie()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find the single different char between two strings.
A: Constraints
- Can we assume the strings are ASCII?
  - Yes
- Is case important?
  - The strings are lower case
- Can we assume the inputs are valid?
  - No, check for None
- Can we assume this fits memory?
  - Yes
Test Cases
- None input -> TypeError
- 'abcd', 'abcde' -> 'e'
- 'aaabbcdd', 'abdbacade' -> 'e'
Algorithm
Dictionary
- Keep a dictionary of seen values in s
- Loop through t, decrementing the seen values
  - If the char is not there or if the decrement results in a negative value, return the char
Complexity:
- Time: O(m+n), where m and n are the lengths of s, t
- Space: O(h), for the dict, where h is the unique chars in s
XOR
- XOR the two strings, which will isolate the differing char
Complexity:
- Time: O(m+n), where m and n are the lengths of s, t
- Space: O(1), for the dict, where h is the unique chars in s
Code
class Solution(object):
​
    def find_diff(self, str1, str2):
        if str1 is None or str2 is None:
            raise TypeError('str1 or str2 cannot be None')
        seen = {}
        for char in str1:
            if char in seen:
                seen[char] += 1
            else:
                seen[char] = 1
        for char in str2:
            try:
                seen[char] -= 1
            except KeyError:
                return char
            if seen[char] < 0:
                return char
        return None
​
    def find_diff_xor(self, str1, str2):
        if str1 is None or str2 is None:
            raise TypeError('str1 or str2 cannot be None')
        result = 0
        for char in str1:
            result ^= ord(char)
        for char in str2:
            result ^= ord(char)
        return chr(result)
Unit Test
%%writefile test_str_diff.py
from nose.tools import assert_equal, assert_raises
​
​
class TestFindDiff(object):
​
    def test_find_diff(self):
        solution = Solution()
        assert_raises(TypeError, solution.find_diff, None)
        assert_equal(solution.find_diff('abcd', 'abcde'), 'e')
        assert_equal(solution.find_diff('aaabbcdd', 'abdbacade'), 'e')
        print('Success: test_find_diff')
​
​
def main():
    test = TestFindDiff()
    test.test_find_diff()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Island Perimeter.
See the LeetCode problem page.
You are given a map in form of a two-dimensional integer grid where 1 represents land and 0 represents water. Grid cells are connected horizontally/vertically (not diagonally). The grid is completely surrounded by water, and there is exactly one island (i.e., one or more connected land cells). The island doesn't have "lakes" (water inside that isn't connected to the water around the island). One cell is a square with side length 1. The grid is rectangular, width and height don't exceed 100. Determine the perimeter of the island.
Example:
[[0,1,0,0],
 [1,1,1,0],
 [0,1,0,0],
 [1,1,0,0]]
Answer: 16
A: Constraints
- Can we assume the inputs are valid?
  - No, check for None
- Can we assume this fits memory?
  - Yes
Test Cases
* None -> TypeError
* [[1, 0]] -> 4
* [[0, 1, 0, 0],
   [1, 1, 1, 0],
   [0, 1, 0, 0],
   [1, 1, 0, 0]] -> 16
Algorithm
For each cell in the grid:
- Check left, right, up, down
  - For each check, if we are at the edge or the cell we are checking is land, increment sides
Complexity:
- Time: O(rows * cols)
- Space: O(1)
Code
class Solution(object):
​
    def island_perimeter(self, grid):
        if grid is None:
            raise TypeError('grid cannot be None')
        sides = 0
        num_rows = len(grid)
        num_cols = len(grid[0])
        for i in range(num_rows):
            for j in range(num_cols):
                if grid[i][j] == 1:
                    # Check left
                    if j == 0 or grid[i][j - 1] == 0:
                        sides += 1
                    # Check right
                    if j == num_cols - 1 or grid[i][j + 1] == 0:
                        sides += 1
                    # Check up
                    if i == 0 or grid[i - 1][j] == 0:
                        sides += 1
                    # Check down
                    if i == num_rows - 1 or grid[i + 1][j] == 0:
                        sides += 1
        return sides
Unit Test
%%writefile test_island_perimeter.py
from nose.tools import assert_equal, assert_raises
​
​
class TestIslandPerimeter(object):
​
    def test_island_perimeter(self):
        solution = Solution()
        assert_raises(TypeError, solution.island_perimeter, None)
        data = [[1, 0]]
        expected = 4
        assert_equal(solution.island_perimeter(data), expected)
        data = [[0, 1, 0, 0],
                [1, 1, 1, 0],
                [0, 1, 0, 0],
                [1, 1, 0, 0]]
        expected = 16
        assert_equal(solution.island_perimeter(data), expected)
        print('Success: test_island_perimeter')
​
​
def main():
    test = TestIslandPerimeter()
    test.test_island_perimeter()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find the difference of two integers without using the + or - sign.
A: Constraints
- Can we assume the inputs are valid?
  - No, check for None
- Can we assume this fits memory?
  - Yes
Test Cases
* None input -> TypeError
* 7, 5 -> 2
* -5, -7 -> 2
* -5, 7 -> -12
* 5, -7 -> 12
Algorithm
We'll look at the following example, subtracting a and b:
a 0110 = 6
b 0101 = 5
First, subtract a and b, without worrying about the borrow (0-0=0, 0-1=1, 1-1=0):
result = a ^ b = 0011
Next, calculate the borrow (0-1=1). We'll need to left shift one to prepare for the next iteration when we move to the next most significant bit:
~a = 1001 b = 0101 ~a & b = 0001
borrow = (~a&b) << 1 = 0010
If the borrow is not zero, we'll need to subtract the borrow from the result. Recusively call the function, passing in result and borrow.
Complexity:
- Time: O(b), where b is the number of bits
- Space: O(b), where b is the number of bits
Code
class Solution(object):
​
    def sub_two(self, a, b):
        if a is None or b is None:
            raise TypeError('a or b cannot be None')
        result = a ^ b;
        borrow = (~a & b) << 1
        if borrow != 0:
            return self.sub_two(result, borrow)
        return result;
Unit Test
%%writefile test_sub_two.py
from nose.tools import assert_equal, assert_raises
​
​
class TestSubTwo(object):
​
    def test_sub_two(self):
        solution = Solution()
        assert_raises(TypeError, solution.sub_two, None)
        assert_equal(solution.sub_two(7, 5), 2)
        assert_equal(solution.sub_two(-5, -7), 2)
        assert_equal(solution.sub_two(-5, 7), -12)
        assert_equal(solution.sub_two(5, -7), 12)
        print('Success: test_sub_two')
​
​
def main():
    test = TestSubTwo()
    test.test_sub_two()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find the sum of two integers without using the + or - sign.
A: Constraints
- Can we assume we are working with 32 bit numbers?
  - Yes
- Can we assume the inputs are valid?
  - No, check for None
- Can we assume this fits memory?
  - Yes
Test Cases
* None input -> TypeError
* 5, 7 -> 12
* -5, -7 -> -12
* 5, -7 -> -2
* -1, 1 -> 0
Algorithm
We'll look at the following example, adding a and b:
a 0111
b 0101
First, add a and b, without worrying about the carry (0+0=0, 0+1=1, 1+1=0):
result = a ^ b = 0010
Next, calculate the carry (1+1=2). We'll need to left shift one to prepare for the next iteration when we move to the next most significant bit:
carry = (a&b) << 1 = 1010
If the carry is not zero, we'll need to add the carry to the result. Recusively call the function, passing in result and carry.
Below are the values of a, b, and the carry of a = 7 and b = 5, producing the result of 12.
a 0111
b 0101
-----
c 0101
a 0010
b 1010
-----
c 0010
a 1000
b 0100
-----
c 0000
a 1100
b 0000
c = carry = 0, return the result 1100
Complexity:
- Time: O(b), where b is the number of bits
- Space: O(b), where b is the number of bits
Note: Doing this in Python is more complicated than say, a Java implementation.
Code
class Solution(object):
​
    def sum_of_two_bugged(self, a, b):
        if a is None or b is None:
            raise TypeError('a or b cannot be None')
        result = a ^ b;
        carry = (a & b) << 1
        if carry != 0:
            return self.sum_of_two(result, carry)
        return result;
​
    def sum_of_two(self, a, b):
        if a is None or b is None:
            raise TypeError('a or b cannot be None')
        MAX = 0x7FFFFFFF  # 32 bit integer max valid value
        MASK = 0xFFFFFFFF  # Get the last 32 bits
        result = (a ^ b) & MASK
        carry = ((a & b) << 1) & MASK
        if carry != 0:
            return self.sum_of_two(result, carry)
        if result <= MAX:
            return result
        else:
            # Get the 32-bit positive complement with ^
            # and convert to the Python complement with ~
            return ~(result ^ MASK)
Unit Test
%%writefile test_sum_of_two.py
from nose.tools import assert_equal, assert_raises
​
​
class TestSumOfTwo(object):
​
    def test_sum_of_two(self):
        solution = Solution()
        assert_raises(TypeError, solution.sum_of_two, None)
        assert_equal(solution.sum_of_two(5, 7), 12)
        assert_equal(solution.sum_of_two(-5, -7), -12)
        assert_equal(solution.sum_of_two(5, -7), -2)
        assert_equal(solution.sum_of_two(-1, 1), 0)
        print('Success: test_sum_of_two')
​
​
def main():
    test = TestSumOfTwo()
    test.test_sum_of_two()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Given an int, repeatedly add its digits until the result is one digit.
A: Constraints
- Can we assume num is not negative?
  - Yes
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
* None input -> TypeError
* negative input -> ValueError
* 9 -> 9
* 138 -> 3
* 65536 -> 7
Algorithm
The naive solution simply isolates each digit with with modulo and integer division. We'll add each isolated digit to a list and sum the values.
138 % 10 = 8 -> isolated
138 // 10 = 13
13 % 10 = 3 -> isolated
13 // 10 = 1
1 % 10 = 1 -> isolated
A more optimal solution exists, by recognizing this is a digital root. See the Wikipedia article for more information.
Complexity:
- Time: O(1)
- Space: O(1)
Code
class Solution(object):
​
    def add_digits(self, num):
        if num is None:
            raise TypeError('num cannot be None')
        if num < 0:
            raise ValueError('num cannot be negative')
        digits = []
        while num != 0:
            digits.append(num % 10)
            num //= 10
        digits_sum = sum(digits)
        if digits_sum >= 10:
            return self.add_digits(digits_sum)
        else:
            return digits_sum
​
    def add_digits_optimized(self, num):
        if num is None:
            raise TypeError('num cannot be None')
        if num < 0:
            raise ValueError('num cannot be negative')
        if num == 0:
            return 0
        elif num % 9 == 0:
            return 9
        else:
            return num % 9
Unit Test
%%writefile test_add_digits.py
from nose.tools import assert_equal, assert_raises
​
​
class TestAddDigits(object):
​
    def test_add_digits(self, func):
        assert_raises(TypeError, func, None)
        assert_raises(ValueError, func, -1)
        assert_equal(func(0), 0)
        assert_equal(func(9), 9)
        assert_equal(func(138), 3)
        assert_equal(func(65536), 7)
        print('Success: test_add_digits')
​
​
def main():
    test = TestAddDigits()
    solution = Solution()
    test.test_add_digits(solution.add_digits)
    try:
        test.test_add_digits(solution.add_digits_optimized)
    except NameError:
        # Alternate solutions are only defined
        # in the solutions file
        pass
​
​
if __name__ == '__main__':
    main()

Q: Problem: Move all zeroes in a list to the end.
A: Constraints
- Is the input an array of ints?
  - Yes
- Is the output a new array of ints?
  - No, do this in-place
- Do we need to maintain ordering of non-zero values?
  - Yes
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
* None -> TypeError
* [0, 1, 0, 3, 12] -> [1, 3, 12, 0, 0]
* [1, 0] -> [1, 0]
* [0, 1] -> [1, 0]
* [0] -> [0]
* [1] -> [1]
* [1, 1] -> [1, 1]
Algorithm
- pos = 0
- Loop through each item in the input
  - If the item != 0, set input[pos] = item
    - pos++
- Fill input[pos:] with zeroes
 |
[0, 1, 0, 3, 12]
 ^
    |
[0, 1, 0, 3, 12]
 ^
    |
[1, 1, 0, 3, 12]
 ^
       |
[1, 1, 0, 3, 12]
    ^
          |
[1, 1, 0, 3, 12]
    ^
          |
[1, 3, 0, 3, 12]
    ^
              |
[1, 3, 0, 3, 12]
       ^
              |
[1, 3, 12, 3, 12]
       ^
Fill right with zeroes:
[1, 3, 12, 3, 12]
           ^
[1, 3, 12, 0, 12]
           ^
[1, 3, 12, 0, 0]
              ^
Complexity:
- Time: O(n)
- Space: O(1)
Code
class Solution(object):
​
    def move_zeroes(self, nums):
        if nums is None:
            raise TypeError('nums cannot be None')
        pos = 0
        for num in nums:
            if num != 0:
                nums[pos] = num
                pos += 1
        if pos < len(nums):
            nums[pos:] = [0] * (len(nums) - pos)
Unit Test
%%writefile test_move_zeroes.py
from nose.tools import assert_equal, assert_raises
​
​
class TestMoveZeroes(object):
​
    def test_move_zeroes(self):
        solution = Solution()
        assert_raises(TypeError, solution.move_zeroes, None)
        array = [0, 1, 0, 3, 12]
        solution.move_zeroes(array)
        assert_equal(array, [1, 3, 12, 0, 0])
        array = [1, 0]
        solution.move_zeroes(array)
        assert_equal(array, [1, 0])
        array = [0, 1]
        solution.move_zeroes(array)
        assert_equal(array, [1, 0])
        array = [0]
        solution.move_zeroes(array)
        assert_equal(array, [0])
        array = [1]
        solution.move_zeroes(array)
        assert_equal(array, [1])
        array = [1, 1]
        solution.move_zeroes(array)
        assert_equal(array, [1, 1])
        print('Success: test_move_zeroes')
​
​
def main():
    test = TestMoveZeroes()
    test.test_move_zeroes()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Given an array, find the two indices that sum to a specific value.
A: Constraints
- Is there exactly one solution?
  - Yes
- Is there always a solution?
  - Yes
- Is the array an array of ints?
  - Yes
- Is the array sorted? No
- Are negative values possible?
  - Yes
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None input -> TypeError
- [] -> ValueError
- [1, 3, 2, -7, 5], 7 -> [2, 4]
Algorithm
Brute force
- For i in range(len(input)):
  - For j in range(i+1, len(input)):
    - if i + j == target return True
- return False
Complexity:
- Time: O(n^2)
- Space: O(1)
Optimized
* Loop through each num in nums
    * Calculate the cache_target = target - num
target = 7
index  =  0  1  2   3  4
nums   = [1, 3, 2, -7, 5]
          ^
cache_target = 7 - 1 = 6
cache
6 -> 0
1 not in cache
index  =  0  1  2   3  4
nums   = [1, 3, 2, -7, 5]
             ^
cache_target = 7 - 3 = 4
cache
6 -> 0
4 -> 1
3 not in cache
index  =  0  1  2   3  4
nums   = [1, 3, 2, -7, 5]
                ^
cache_target = 7 - 2 = 5
cache
6 -> 0
4 -> 1
5 -> 2
2 not in cache
index  =  0  1  2   3  4
nums   = [1, 3, 2, -7, 5]
                    ^
cache_target = 7 + 7 = 14
cache
6  -> 0
4  -> 1
5  -> 2
14 -> 3
-7 not in cache
index  =  0  1  2   3  4
nums   = [1, 3, 2, -7, 5]
                       ^
cache_target = 7 - 5 = 2
cache
6  -> 0
4  -> 1
5  -> 2
14 -> 3
5 in cache, success, output matching indices: cache[num] and current iteration index
output = [2, 4]
Complexity:
- Time: O(n)
- Space: O(n)
Code
class Solution(object):
​
    def two_sum(self, nums, target):
        if nums is None or target is None:
            raise TypeError('nums or target cannot be None')
        if not nums:
            raise ValueError('nums cannot be empty')
        cache = {}
        for index, num in enumerate(nums):
            cache_target = target - num
            if num in cache:
                return [cache[num], index]
            else:
                cache[cache_target] = index
        return None
Unit Test
%%writefile test_two_sum.py
from nose.tools import assert_equal, assert_raises
​
​
class TestTwoSum(object):
​
    def test_two_sum(self):
        solution = Solution()
        assert_raises(TypeError, solution.two_sum, None, None)
        assert_raises(ValueError, solution.two_sum, [], 0)
        target = 7
        nums = [1, 3, 2, -7, 5]
        expected = [2, 4]
        assert_equal(solution.two_sum(nums, target), expected)
        print('Success: test_two_sum')
​
​
def main():
    test = TestTwoSum()
    test.test_two_sum()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Given a knapsack with a total weight capacity and a list of items with weight w(i) and value v(i), determine which items to select to maximize total value.
A: Constraints
- Can we replace the items once they are placed in the knapsack?
  - No, this is the 0/1 knapsack problem
- Can we split an item?
  - No
- Can we get an input item with weight of 0 or value of 0?
  - No
- Can we assume the inputs are valid?
  - No
- Are the inputs in sorted order by val/weight?
  - Yes, if not we'd need to sort them first
- Can we assume this fits memory?
  - Yes
Test Cases
- items or total weight is None -> Exception
- items or total weight is 0 -> 0
- General case
total_weight = 8
items
  v | w
  0 | 0
a 2 | 2
b 4 | 2
c 6 | 4
d 9 | 5
max value = 13
items
  v | w
b 4 | 2
d 9 | 5
Algorithm
We'll use bottom up dynamic programming to build a table.
The solution for the top down approach is also provided below.
v = value
w = weight
               j
    -------------------------------------------------
    | v | w || 0 | 1 | 2 | 3 | 4 | 5 | 6  | 7  | 8  |
    -------------------------------------------------
    | 0 | 0 || 0 | 0 | 0 | 0 | 0 | 0 | 0  | 0  | 0  |
i a | 2 | 2 || 0 | 0 | 2 | 2 | 2 | 2 | 2  | 2  | 2  |
  b | 4 | 2 || 0 | 0 | 4 | 4 | 6 | 6 | 6  | 6  | 6  |
  c | 6 | 4 || 0 | 0 | 4 | 4 | 6 | 6 | 10 | 10 | 12 |
  d | 9 | 5 || 0 | 0 | 4 | 4 | 6 | 9 | 10 | 13 | 13 |
    -------------------------------------------------
i = row
j = col
if j >= item[i].weight:
    T[i][j] = max(item[i].value + T[i - 1][j - item[i].weight],
                  T[i - 1][j])
else:
    T[i][j] = T[i - 1][j]
Complexity:
- Time: O(n * w), where n is the number of items and w is the total weight
- Space: O(n * w), where n is the number of items and w is the total weight
Code
Item Class
class Item(object):
​
    def __init__(self, label, value, weight):
        self.label = label
        self.value = value
        self.weight = weight
​
    def __repr__(self):
        return self.label + ' v:' + str(self.value) + ' w:' + str(self.weight)
Knapsack Bottom Up
class Knapsack(object):
​
    def fill_knapsack(self, input_items, total_weight):
        if input_items is None or total_weight is None:
            raise TypeError('input_items or total_weight cannot be None')
        if not input_items or total_weight == 0:
            return 0
        items = list([Item(label='', value=0, weight=0)] + input_items)
        num_rows = len(items)
        num_cols = total_weight + 1
        T = [[None] * num_cols for _ in range(num_rows)]
        for i in range(num_rows):
            for j in range(num_cols):
                if i == 0 or j == 0:
                    T[i][j] = 0
                elif j >= items[i].weight:
                    T[i][j] = max(items[i].value + T[i - 1][j - items[i].weight],
                                  T[i - 1][j])
                else:
                    T[i][j] = T[i - 1][j]
        results = []
        i = num_rows - 1
        j = num_cols - 1
        while T[i][j] != 0:
            if T[i - 1][j] ==  T[i][j]:
                i -= 1
            elif T[i][j - 1] ==  T[i][j]:
                j -= 1
            else:
                results.append(items[i])
                i -= 1
                j -= items[i].weight
        return results
Knapsack Top Down
class KnapsackTopDown(object):
​
    def fill_knapsack(self, items, total_weight):
        if items is None or total_weight is None:
            raise TypeError('input_items or total_weight cannot be None')
        if not items or not total_weight:
            return 0
        memo = {}
        result = self._fill_knapsack(items, total_weight, memo, index=0)
        return result
​
​
    def _fill_knapsack(self, items, total_weight, memo, index):
        if total_weight < 0:
            return 0
        if not total_weight or index >= len(items):
            return items[index - 1].value
        if (total_weight, len(items) - index - 1) in memo:
            return memo[(total_weight, len(items) - index - 1)] + items[index - 1].value
        results = []
        for i in range(index, len(items)):
            total_weight -= items[i].weight
            result = self._fill_knapsack(items, total_weight, memo, index=i + 1)
            total_weight += items[i].weight
            results.append(result)
        results_index = 0
        for i in range(index, len(items)):
            memo[total_weight, len(items) - i] = max(results[results_index:])
            results_index += 1
        return max(results) + (items[index - 1].value if index > 0 else 0)
Knapsack Top Down Alternate
class Result(object):
​
    def __init__(self, total_weight, item):
        self.total_weight = total_weight
        self.item = item
​
    def __repr__(self):
        return 'w:' + str(self.total_weight) + ' i:' + str(self.item)
​
    def __lt__(self, other):
        return self.total_weight < other.total_weight
​
​
def knapsack_top_down_alt(items, total_weight):
    if items is None or total_weight is None:
        raise TypeError('input_items or total_weight cannot be None')
    if not items or not total_weight:
        return 0
    memo = {}
    result = _knapsack_top_down_alt(items, total_weight, memo, index=0)
    curr_item = result.item
    curr_weight = curr_item.weight
    picked_items = [curr_item]
    while curr_weight > 0:
        total_weight -= curr_item.weight
        curr_item = memo[(total_weight, len(items) - len(picked_items))].item
    return result
​
​
def _knapsack_top_down_alt(items, total_weight, memo, index):
    if total_weight < 0:
        return Result(total_weight=0, item=None)
    if not total_weight or index >= len(items):
        return Result(total_weight=items[index - 1].value, item=items[index - 1])
    if (total_weight, len(items) - index - 1) in memo:
        weight=memo[(total_weight,
                     len(items) - index - 1)].total_weight + items[index - 1].value
        return Result(total_weight=weight,
                      item=items[index-1])
    results = []
    for i in range(index, len(items)):
        total_weight -= items[i].weight
        result = _knapsack_top_down_alt(items, total_weight, memo, index=i + 1)
        total_weight += items[i].weight
        results.append(result)
    results_index = 0
    for i in range(index, len(items)):
        memo[(total_weight, len(items) - i)] = max(results[results_index:])
        results_index += 1
    if index == 0:
        result_item = memo[(total_weight, len(items) - 1)].item
    else:
        result_item = items[index - 1]
    weight = max(results).total_weight + (items[index - 1].value if index > 0 else 0)
    return Result(total_weight=weight,
                  item=result_item)
Unit Test
%%writefile test_knapsack.py
from nose.tools import assert_equal, assert_raises
​
​
class TestKnapsack(object):
​
    def test_knapsack_bottom_up(self):
        knapsack = Knapsack()
        assert_raises(TypeError, knapsack.fill_knapsack, None, None)
        assert_equal(knapsack.fill_knapsack(0, 0), 0)
        items = []
        items.append(Item(label='a', value=2, weight=2))
        items.append(Item(label='b', value=4, weight=2))
        items.append(Item(label='c', value=6, weight=4))
        items.append(Item(label='d', value=9, weight=5))
        total_weight = 8
        expected_value = 13
        results = knapsack.fill_knapsack(items, total_weight)
        assert_equal(results[0].label, 'd')
        assert_equal(results[1].label, 'b')
        total_value = 0
        for item in results:
            total_value += item.value
        assert_equal(total_value, expected_value)
        print('Success: test_knapsack_bottom_up')
​
    def test_knapsack_top_down(self):
        knapsack = KnapsackTopDown()
        assert_raises(TypeError, knapsack.fill_knapsack, None, None)
        assert_equal(knapsack.fill_knapsack(0, 0), 0)
        items = []
        items.append(Item(label='a', value=2, weight=2))
        items.append(Item(label='b', value=4, weight=2))
        items.append(Item(label='c', value=6, weight=4))
        items.append(Item(label='d', value=9, weight=5))
        total_weight = 8
        expected_value = 13
        assert_equal(knapsack.fill_knapsack(items, total_weight), expected_value)
        print('Success: test_knapsack_top_down')
​
def main():
    test = TestKnapsack()
    test.test_knapsack_bottom_up()
    test.test_knapsack_top_down()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Given a knapsack with a total weight capacity and a list of items with weight w(i) and value v(i), determine the max total value you can carry.
A: Constraints
- Can we replace the items once they are placed in the knapsack?
  - Yes, this is the unbounded knapsack problem
- Can we split an item?
  - No
- Can we get an input item with weight of 0 or value of 0?
  - No
- Do we need to return the items that make up the max total value?
  - No, just the total value
- Can we assume the inputs are valid?
  - No
- Are the inputs in sorted order by val/weight?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- items or total weight is None -> Exception
- items or total weight is 0 -> 0
- General case
total_weight = 8
items
  v | w
  0 | 0
a 1 | 1
b 3 | 2
c 7 | 4
max value = 14
Algorithm
We'll use bottom up dynamic programming to build a table.
Taking what we learned with the 0/1 knapsack problem, we could solve the problem like the following:
v = value
w = weight
               j
    -------------------------------------------------
    | v | w || 0 | 1 | 2 | 3 | 4 | 5 |  6 |  7 |  8  |
    -------------------------------------------------
    | 0 | 0 || 0 | 0 | 0 | 0 | 0 | 0 |  0 |  0 |  0  |
  a | 1 | 1 || 0 | 1 | 2 | 3 | 4 | 5 |  6 |  7 |  8  |
i b | 3 | 2 || 0 | 1 | 3 | 4 | 6 | 7 |  9 | 10 | 12  |
  c | 7 | 4 || 0 | 1 | 3 | 4 | 7 | 8 | 10 | 11 | 14  |
    -------------------------------------------------
i = row
j = col
However, unlike the 0/1 knapsack variant, we don't actually need to keep space of O(n * w), where n is the number of items and w is the total weight. We just need a single array that we update after we process each item:
    -------------------------------------------------
    | v | w || 0 | 1 | 2 | 3 | 4 | 5 |  6 |  7 |  8  |
    -------------------------------------------------
    -------------------------------------------------
  a | 1 | 1 || 0 | 1 | 2 | 3 | 4 | 5 |  6 |  7 |  8  |
    -------------------------------------------------
    -------------------------------------------------
  b | 3 | 2 || 0 | 1 | 3 | 4 | 6 | 7 |  9 | 10 | 12  |
    -------------------------------------------------
    -------------------------------------------------
  c | 7 | 4 || 0 | 1 | 3 | 4 | 7 | 8 | 10 | 11 | 14  |
    -------------------------------------------------
if j >= items[i].weight:
    T[j] = max(items[i].value + T[j - items[i].weight],
               T[j])
Complexity:
- Time: O(n * w), where n is the number of items and w is the total weight
- Space: O(w), where w is the total weight
Code
Item Class
class Item(object):
​
    def __init__(self, label, value, weight):
        self.label = label
        self.value = value
        self.weight = weight
​
    def __repr__(self):
        return self.label + ' v:' + str(self.value) + ' w:' + str(self.weight)
Knapsack Bottom Up
class Knapsack(object):
​
    def fill_knapsack(self, items, total_weight):
        if items is None or total_weight is None:
            raise TypeError('items or total_weight cannot be None')
        if not items or total_weight == 0:
            return 0
        num_rows = len(items)
        num_cols = total_weight + 1
        T = [0] * (num_cols)
        for i in range(num_rows):
            for j in range(num_cols):
                if j >= items[i].weight:
                    T[j] = max(items[i].value + T[j - items[i].weight],
                               T[j])
        return T[-1]
Unit Test
%%writefile test_knapsack_unbounded.py
from nose.tools import assert_equal, assert_raises
​
​
class TestKnapsack(object):
​
    def test_knapsack(self):
        knapsack = Knapsack()
        assert_raises(TypeError, knapsack.fill_knapsack, None, None)
        assert_equal(knapsack.fill_knapsack(0, 0), 0)
        items = []
        items.append(Item(label='a', value=1, weight=1))
        items.append(Item(label='b', value=3, weight=2))
        items.append(Item(label='c', value=7, weight=4))
        total_weight = 8
        expected_value = 14
        results = knapsack.fill_knapsack(items, total_weight)
        total_weight = 7
        expected_value = 11
        results = knapsack.fill_knapsack(items, total_weight)
        assert_equal(results, expected_value)
        print('Success: test_knapsack')
​
def main():
    test = TestKnapsack()
    test.test_knapsack()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Given a magazine, see if a ransom note could have been written using the letters in the magazine.
A: Constraints
- Is this case sensitive?
  - Yes
- Can we assume we're working with ASCII characters?
  - Yes
- Can we scan the entire magazine, or should we scan only when necessary?
  - You can scan the entire magazine
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> Exception
- '', '' -> Exception
- 'a', 'b' -> False
- 'aa', 'ab' -> False
- 'aa', 'aab' -> True
Algorithm
- Build a dictionary of the magazine characters and counts.
- Loop through each letter in the ransom note and see if there are enough letters in the magazine's dictionary.
- Note: You could make this more efficient by not scanning the entire magazine all at once, but instead scan just in time as you run out of letters in the dictionary.
Complexity:
- Time: O(n+m), where n is the length of the ransom note and m is the length of the magazine
- Space: O(n+m)
Code
class Solution(object):
​
    def match_note_to_magazine(self, ransom_note, magazine):
        if ransom_note is None or magazine is None:
            raise TypeError('ransom_note or magazine cannot be None')
        seen_chars = {}
        for char in magazine:
            if char in seen_chars:
                seen_chars[char] += 1
            else:
                seen_chars[char] = 1
        for char in ransom_note:
            try:
                seen_chars[char] -= 1
            except KeyError:
                return False
            if seen_chars[char] < 0:
                return False
        return True
Unit Test
%%writefile test_ransom_note.py
from nose.tools import assert_equal, assert_raises
​
​
class TestRansomNote(object):
​
    def test_ransom_note(self):
        solution = Solution()
        assert_raises(TypeError, solution.match_note_to_magazine, None, None)
        assert_equal(solution.match_note_to_magazine('', ''), True)
        assert_equal(solution.match_note_to_magazine('a', 'b'), False)
        assert_equal(solution.match_note_to_magazine('aa', 'ab'), False)
        assert_equal(solution.match_note_to_magazine('aa', 'aab'), True)
        print('Success: test_ransom_note')
​
​
def main():
    test = TestRansomNote()
    test.test_ransom_note()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find the shortest path between two nodes in an unweighted graph.
A: Constraints
- Is the graph directed?
  - Yes
- Can we assume we already have Graph and Node classes?
  - Yes
- Are the inputs two Nodes?
  - Yes
- Is the output a list of Node keys that make up the shortest path?
  - Yes
- If there is no path, should we return None?
  - Yes
- Can we assume the inputs are valid?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
Input:
- add_edge(source, destination, weight)
graph.add_edge(0, 1)
graph.add_edge(0, 4)
graph.add_edge(0, 5)
graph.add_edge(1, 3)
graph.add_edge(1, 4)
graph.add_edge(2, 1)
graph.add_edge(3, 2)
graph.add_edge(3, 4)
Result:
- search_path(start=0, end=2) -> [0, 1, 3, 2]
- search_path(start=0, end=0) -> [0]
- search_path(start=4, end=5) -> None
Algorithm
To determine the shorted path in an unweighted graph, we can use breadth-first search keeping track of the previous nodes ids for each node. Previous nodes ids can be a dictionary of key: current node id and value: previous node id.
- If the start node is the end node, return True
- Add the start node to the queue and mark it as visited
  - Update the previous node ids, the previous node id of the start node is None
- While the queue is not empty
  - Dequeue a node and visit it
  - If the node is the end node, return the previous nodes
  - Set the previous node to the current node
  - Iterate through each adjacent node
    - If the node has not been visited, add it to the queue and mark it as visited
      - Update the previous node ids
- Return None
Walk the previous node ids backwards to get the path.
Complexity:
- Time: O(V + E), where V = number of vertices and E = number of edges
- Space: O(V + E)
Code
from collections import deque
​
​
class GraphShortestPath(Graph):
​
    def shortest_path(self, source_key, dest_key):
        if source_key is None or dest_key is None:
            return None
        if source_key is dest_key:
            return [source_key]
        prev_node_keys = self._shortest_path(source_key, dest_key)
        if prev_node_keys is None:
            return None
        else:
            path_ids = [dest_key]
            prev_node_key = prev_node_keys[dest_key]
            while prev_node_key is not None:
                path_ids.append(prev_node_key)
                prev_node_key = prev_node_keys[prev_node_key]
            return path_ids[::-1]
​
    def _shortest_path(self, source_key, dest_key):
        queue = deque()
        queue.append(self.nodes[source_key])
        prev_node_keys = {source_key: None}
        self.nodes[source_key].visit_state = State.visited
        while queue:
            node = queue.popleft()
            if node.key is dest_key:
                return prev_node_keys
            prev_node = node
            for adj_node in node.adj_nodes.values():
                if adj_node.visit_state == State.unvisited:
                    queue.append(adj_node)
                    prev_node_keys[adj_node.key] = prev_node.key
                    adj_node.visit_state = State.visited
        return None
Unit Test
%%writefile test_shortest_path.py
from nose.tools import assert_equal
​
​
class TestShortestPath(object):
​
    def test_shortest_path(self):
        nodes = []
        graph = GraphShortestPath()
        for id in range(0, 6):
            nodes.append(graph.add_node(id))
        graph.add_edge(0, 1)
        graph.add_edge(0, 4)
        graph.add_edge(0, 5)
        graph.add_edge(1, 3)
        graph.add_edge(1, 4)
        graph.add_edge(2, 1)
        graph.add_edge(3, 2)
        graph.add_edge(3, 4)
​
        assert_equal(graph.shortest_path(nodes[0].key, nodes[2].key), [0, 1, 3, 2])
        assert_equal(graph.shortest_path(nodes[0].key, nodes[0].key), [0])
        assert_equal(graph.shortest_path(nodes[4].key, nodes[5].key), None)
​
        print('Success: test_shortest_path')
​
​
def main():
    test = TestShortestPath()
    test.test_shortest_path()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Determine whether you can win the Nim game given the remaining stones.
See the LeetCode problem page.
You are playing the following Nim Game with your friend: There is a heap of stones on the table, each time one of you take turns to remove 1 to 3 stones. The one who removes the last stone will be the winner. You will take the first turn to remove the stones.
Both of you are very clever and have optimal strategies for the game. Write a function to determine whether you can win the game given the number of stones in the heap.
For example, if there are 4 stones in the heap, then you will never win the game: no matter 1, 2, or 3 stones you remove, the last stone will always be removed by your friend.
A: Constraints
- Is the input an int?
  - Yes
- Is the output a boolean?
  - Yes
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> TypeError
- 1, 2, or 3 -> True
- 4 -> False
- 7 -> True
- 40 -> False
Algorithm
This is somewhat of a one-trick puzzle, where the only way you can lose if you take the first stone while playing optimally is if the number of remaining stones is divisible by 4.
Complexity:
- Time: O(1)
- Space: O(1)
Code
class Solution(object):
​
    def can_win_nim(self, num_stones_left):
        if not isinstance(num_stones_left, int):
            raise TypeError('num_stones_left must be an int')
        return num_stones_left % 4 != 0
Unit Test
%%writefile test_can_win_nim.py
from nose.tools import assert_equal, assert_raises
​
​
class TestSolution(object):
​
    def test_can_win_nim(self):
        solution = Solution()
        assert_raises(TypeError, solution.can_win_nim, None)
        assert_equal(solution.can_win_nim(1), True)
        assert_equal(solution.can_win_nim(2), True)
        assert_equal(solution.can_win_nim(3), True)
        assert_equal(solution.can_win_nim(4), False)
        assert_equal(solution.can_win_nim(7), True)
        assert_equal(solution.can_win_nim(40), False)
        print('Success: test_can_win_nim')
​
​
def main():
    test = TestSolution()
    test.test_can_win_nim()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find the length of the longest substring with at most k distinct characters.
A: Constraints
- Can we assume the inputs are valid?
  - No
- Can we assume the strings are ASCII?
  - Yes
- Is this case sensitive?
  - Yes
- Is a substring a contiguous block of chars?
  - Yes
- Do we expect an int as a result?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> TypeError
- '', k = 3 -> 0
- 'abcabcdefgghiij', k=3 -> 6
- 'abcabcdefgghighij', k=3 -> 7
Algorithm
We'll use a chars_to_index_map dictionary: char (key) to index (val) map to maintain a sliding window.
The index (val) will keep track of the character index in the input string.
For each character in the string:
- Add the char (key) and index (value) to the map
- If the length of our map is greater than k, then we'll need to eliminate one item
  - Scan the map to find the lowest index and remove it
  - The new lowest index will therefore be incremented by 1
- The max length will be the current index minus the lower index + 1
Complexity:
- Time: O(n * k), where n is the number of chars, k is the length of the map due to the min() call
- Space: O(n)
Code
class Solution(object):
​
    def longest_substr(self, string, k):
        if not isinstance(string, str):
            raise TypeError('string must be of type str')
        if not isinstance(k, int):
            raise TypeError('k must be of type int')
        low_index = 0
        max_length = 0
        chars_to_index_map = {}
        for index, char in enumerate(string):
            chars_to_index_map[char] = index
            if len(chars_to_index_map) > k:
                low_index = min(chars_to_index_map.values())
                del chars_to_index_map[string[low_index]]
                low_index += 1
            max_length = max(max_length, index - low_index + 1)
        return max_length
Unit Test
%%writefile test_longest_substr.py
from nose.tools import assert_equal, assert_raises
​
​
class TestSolution(object):
​
    def test_longest_substr(self):
        solution = Solution()
        assert_raises(TypeError, solution.longest_substr, None)
        assert_equal(solution.longest_substr('', k=3), 0)
        assert_equal(solution.longest_substr('abcabcdefgghiij', k=3), 6)
        assert_equal(solution.longest_substr('abcabcdefgghighij', k=3), 7)
        print('Success: test_longest_substr')
​
​
def main():
    test = TestSolution()
    test.test_longest_substr()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find the longest absolute file path.
See the LeetCode problem page.
Suppose we abstract our file system by a string in the following manner:
The string "dir\n\tsubdir1\n\tsubdir2\n\t\tfile.ext" represents:
dir
    subdir1
    subdir2
        file.ext
The directory dir contains an empty sub-directory subdir1 and a sub-directory subdir2 containing a file file.ext.
The string "dir\n\tsubdir1\n\t\tfile1.ext\n\t\tsubsubdir1\n\tsubdir2\n\t\tsubsubdir2\n\t\t\tfile2.ext" represents:
dir
    subdir1
        file1.ext
        subsubdir1
    subdir2
        subsubdir2
            file2.ext
The directory dir contains two sub-directories subdir1 and subdir2. subdir1 contains a file file1.ext and an empty second-level sub-directory subsubdir1. subdir2 contains a second-level sub-directory subsubdir2 containing a file file2.ext.
We are interested in finding the longest (number of characters) absolute path to a file within our file system. For example, in the second example above, the longest absolute path is "dir/subdir2/subsubdir2/file2.ext", and its length is 32 (not including the double quotes).
Given a string representing the file system in the above format, return the length of the longest absolute path to file in the abstracted file system. If there is no file in the system, return 0.
Note:
The name of a file contains at least a . and an extension.
The name of a directory or sub-directory will not contain a ..
Time complexity required: O(n) where n is the size of the input string.
Notice that a/aa/aaa/file1.txt is not the longest file path, if there is another path aaaaaaaaaaaaaaaaaaaaa/sth.png.
A: Constraints
- Is the input a string?
  - Yes
- Can we assume the input is valid?
  - No
- Will there always be a file in the input?
  - Yes
- Is the output an int?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> TypeError
- 'dir\n\tsubdir1\n\t\tfile1.ext\n\t\tsubsubdir1\n\tsubdir2\n\t\tsubsubdir2\n\t\t\tfile2.ext' -> 32
Algorithm
We'll use a dictionary path_len to keep track of the current character length (values) at each depth (keys). Depth 0 will have a length of 0.
Initialize max_len to 0.
Split the input based on the newline character. Iterate on each resulting line:
- Extract the name by excluding the tab characters
- Calculate the depth length by taking into account the tab characters
- If we are dealing with a file path (there is a '.' in name)
  - Calculate path_len[depth] + len(name) and update max_len if needed
- Else, update path_len[depth + 1] = path_len[depth] + len(name) + 1
  - We add + 1 because each depth is separated by the '/' character
- Return max_len
Complexity:
- Time: O(n)
- Space: O(n)
Code
class Solution(object):
​
    def length_longest_path(self, file_system):
        if not isinstance(file_system, str):
            raise TypeError('file_system must be a string')
        max_len = 0
        path_len = {0: 0}
        for line in file_system.splitlines():
            name = line.lstrip('\t')
            depth = len(line) - len(name)
            if '.' in name:
                max_len = max(max_len, path_len[depth] + len(name))
            else:
                path_len[depth + 1] = path_len[depth] + len(name) + 1
        return max_len
Unit Test
%%writefile test_length_longest_path.py
from nose.tools import assert_equal, assert_raises
​
​
class TestSolution(object):
​
    def test_length_longest_path(self):
        solution = Solution()
        assert_raises(TypeError, solution.length_longest_path, None)
        file_system = 'dir\n\tsubdir1\n\t\tfile1.ext\n\t\tsubsubdir1\n\tsubdir2\n\t\tsubsubdir2\n\t\t\tfile2.ext'
        expected = 32
        assert_equal(solution.length_longest_path(file_system), expected)
        print('Success: test_length_longest_path')
​
​
def main():
    test = TestSolution()
    test.test_length_longest_path()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Find how many times a sentence can fit on a screen.
See the LeetCode problem page.
Given a rows x cols screen and a sentence represented by a list of non-empty words, find how many times the given sentence can be fitted on the screen.
Note:
A word cannot be split into two lines.
The order of words in the sentence must remain unchanged.
Two consecutive words in a line must be separated by a single space.
Total words in the sentence won't exceed 100.
Length of each word is greater than 0 and won't exceed 10.
1 ≤ rows, cols ≤ 20,000.
Example 1:
Input:
rows = 2, cols = 8, sentence = ["hello", "world"]
Output:
1
Explanation:
hello---
world---
The character '-' signifies an empty space on the screen.
Example 2:
Input:
rows = 3, cols = 6, sentence = ["a", "bcd", "e"]
Output:
2
Explanation:
a-bcd-
e-a---
bcd-e-
The character '-' signifies an empty space on the screen.
Example 3:
Input:
rows = 4, cols = 5, sentence = ["I", "had", "apple", "pie"]
Output:
1
Explanation:
I-had
apple
pie-I
had--
The character '-' signifies an empty space on the screen.
A: Constraints
- Can we assume sentence is ASCII?
  - Yes
- Can we assume the inputs are valid?
  - No
- Is the output an integer?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> TypeError
- rows < 0 or cols < 0 -> ValueError
- cols = 0 -> 0
- sentence = '' -> 0
- rows = 2, cols = 8, sentence = ["hello", "world"] -> 1
- rows = 3, cols = 6, sentence = ["a", "bcd", "e"] -> 2
- rows = 4, cols = 5, sentence = ["I", "had", "apple", "pie"] -> 1
Algorithm
It can be relatively straightforward to come up with the brute force solution, check out the method count_sentence_fit_brute_force below.
The optimized solutions is discussed in more depth here.
rows = 4
cols = 6
sentence = ['abc', 'de', 'f']
"abc de f abc de f abc de f ..." // start=0
 012345                          // start=start+cols+adjustment=0+6+1=7 (1 space removed in screen string)
        012345                   // start=7+6+0=13
              012345             // start=13+6-1=18 (1 space added)
                   012345        // start=18+6+1=25 (1 space added)
                          012345
Complexity:
- Time: O(1)
- Space: O(1)
Code
class Solution(object):
​
    def count_sentence_fit_brute_force(self, sentence, rows, cols):
        try:
            iter(sentence)
        except TypeError:
            raise TypeError('sentence must be of type str')
        if not isinstance(rows, int) or not isinstance(cols, int):
            raise TypeError('rows and cols must be of type int')
        if rows < 0 or cols < 0:
            raise ValueError('rows and cols cannot be negative')
        if cols == 0 or not sentence:
            return 0
        curr_row = 0
        curr_col = 0
        count = 0
        while curr_row < cols:
            for word in sentence:
                # If the current word doesn't fit on the current line,
                # move to the next line
                if len(word) > cols - curr_col:
                    curr_col = 0
                    curr_row += 1
                # If we are beyond the number of rows, return
                if curr_row >= rows:
                    return count
                # If the current word fits on the current line,
                # 'insert' it here
                if len(word) <= cols - curr_col:
                    curr_col += len(word) + 1
                # If it still doesn't fit, then the word is too long
                # and we should just return the current count
                else:
                    return count
            count += 1
        return count
​
    def count_sentence_fit(self, sentence, rows, cols):
        try:
            iter(sentence)
        except TypeError:
            raise TypeError('sentence must be of type str')
        if not isinstance(rows, int) or not isinstance(cols, int):
            raise TypeError('rows and cols must be of type int')
        if rows < 0 or cols < 0:
            raise ValueError('rows and cols cannot be negative')
        if cols == 0 or not sentence:
            return 0
        string = ' '.join(sentence) + ' '
        start = 0
        str_len = len(string)
        for row in range(rows):
            start += cols
            # We don't need extra space for the current row
            if string[start % str_len] == ' ':
                start += 1
            # The current row can't fit, so we'll need to
            # remove characters from the next word
            else:
                while (start > 0 and string[(start - 1) % str_len] != ' '):
                    start -= 1
        return start // str_len
Unit Test
%%writefile test_count_sentence_fit.py
from nose.tools import assert_equal, assert_raises
​
​
class TestSolution(object):
​
    def test_count_sentence_fit(self):
        solution = Solution()
        assert_raises(TypeError, solution.count_sentence_fit,
                      None, None, None)
        assert_raises(ValueError, solution.count_sentence_fit,
                      'abc', rows=-1, cols=-1)
        sentence = ["hello", "world"]
        expected = 1
        assert_equal(solution.count_sentence_fit(sentence, rows=2, cols=8),
                     expected)
        sentence = ["a", "bcd", "e"]
        expected = 2
        assert_equal(solution.count_sentence_fit(sentence, rows=3, cols=6),
                     expected)
        sentence = ["I", "had", "apple", "pie"]
        expected = 1
        assert_equal(solution.count_sentence_fit(sentence, rows=4, cols=5),
                     expected)
        print('Success: test_count_sentence_fit')
​
​
def main():
    test = TestSolution()
    test.test_count_sentence_fit()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Format license keys.
See the LeetCode problem page.
Now you are given a string S, which represents a software license key which we would like to format. The string S is composed of alphanumerical characters and dashes. The dashes split the alphanumerical characters within the string into groups. (i.e. if there are M dashes, the string is split into M+1 groups). The dashes in the given string are possibly misplaced.
We want each group of characters to be of length K (except for possibly the first group, which could be shorter, but still must contain at least one character). To satisfy this requirement, we will reinsert dashes. Additionally, all the lower case letters in the string must be converted to upper case.
So, you are given a non-empty string S, representing a license key to format, and an integer K. And you need to return the license key formatted according to the description above.
Example 1:
Input: S = "2-4A0r7-4k", K = 4
Output: "24A0-R74K"
Explanation: The string S has been split into two parts, each part has 4 characters.
Example 2:
Input: S = "2-4A0r7-4k", K = 3
Output: "24-A0R-74K"
Explanation: The string S has been split into three parts, each part has 3 characters except the first part as it could be shorter as said above.
Note:
The length of string S will not exceed 12,000, and K is a positive integer.
String S consists only of alphanumerical characters (a-z and/or A-Z and/or 0-9) and dashes(-).
String S is non-empty.
A: Constraints
- Is the output a string?
  - Yes
- Can we change the input string?
  - No, you can't modify the input string
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> TypeError
- '---', k=3 -> ''
- '2-4A0r7-4k', k=3 -> '24-A0R-74K'
- '2-4A0r7-4k', k=4 -> '24A0-R74K'
Algorithm
- Loop through each character in the license key backwards, keeping a count of the number of chars we've reached so far, while inserting each character into a result list (convert to upper case)
  - If we reach a '-', skip it
  - Whenever we reach a char count of k, append a '-' character to the result list, reset the char count
- Careful that we don't have a leading '-', which we might hit with test case: '2-4A0r7-4k', k=4 -> '24A0-R74K'
- Reverse the result list and return it
Complexity:
- Time: O(n)
- Space: O(n)
Code
1
class Solution(object):
2
​
3
    def format_license_key(self, license_key, k):
4
        if not isinstance(license_key, str):
5
            raise TypeError('license_key must be a str')
6
        if not isinstance(k, int):
7
            raise TypeError('k must be an int')
8
        if not license_key:
9
            raise ValueError('license_key must not be empty')
10
        formatted_license_key = []
11
        num_chars = 0
12
        for char in license_key[::-1]:
13
            if char == '-':
14
                continue
15
            num_chars += 1
16
            formatted_license_key.append(char.upper())
17
            if num_chars >= k:
18
                formatted_license_key.append('-')
19
                num_chars = 0
20
        if formatted_license_key and formatted_license_key[-1] == '-':
21
            formatted_license_key.pop(-1)
22
        return ''.join(formatted_license_key[::-1])
Unit Test
1
%%writefile test_format_license_key.py
2
from nose.tools import assert_equal, assert_raises
3
​
4
​
5
class TestSolution(object):
6
​
7
    def test_format_license_key(self):
8
        solution = Solution()
9
        assert_raises(TypeError, solution.format_license_key, None, None)
10
        license_key = '---'
11
        k = 3
12
        expected = ''
13
        assert_equal(solution.format_license_key(license_key, k), expected)
14
        license_key = '2-4A0r7-4k'
15
        k = 3
16
        expected = '24-A0R-74K'
17
        assert_equal(solution.format_license_key(license_key, k), expected)
18
        license_key = '2-4A0r7-4k'
19
        k = 4
20
        expected = '24A0-R74K'
21
        assert_equal(solution.format_license_key(license_key, k), expected)
22
        print('Success: test_format_license_key')
23
​
24
def main():
25
    test = TestSolution()
26
    test.test_format_license_key()
27
​
28
​
29
if __name__ == '__main__':
30
    main()

Q: Problem: Given an array of (unix_timestamp, num_people, EventType.ENTER or EventType.EXIT), find the busiest period.
A: Constraints
- Can we assume the input array is valid?
  - Check for None
- Can we assume the elements of the input array are valid?
  - Yes
- Is the input sorted by time?
  - No
- Can you have enter and exit elements for the same timestamp?
  - Yes you can, order of enter and exit is not guaranteed
- Could we have multiple enter events (or multiple exit events) for the same timestamp?
  - No
- What is the format of the output?
  - An array of timestamps [t1, t2]
- Can we assume the starting number of people is zero?
  - Yes
- Can we assume the inputs are valid?
  - No
- Can we assume this fits memory?
  - Yes
Test Cases
- None -> TypeError
- [] -> None
- General case
timestamp  num_people  event_type
3          2           EventType.EXIT
1          2           EventType.ENTER
3          1           EventType.ENTER
7          3           EventType.ENTER
9          2           EventType.EXIT
8          2           EventType.EXIT
result = Period(7, 8)
Algorithm
Since the input is not sorted, we'll need to sort it first by timestamp, ascending.
For each interval in the data set:
- If this is an "enter" event, increment curr_people, else, decrement
- Since we can have an "enter" and "exit" event for the same timestamp, we'll need to look ahead one
  - If the next element has the same timestamp, hold off (continue) on updating max_people and max_period
  - Watch out for indexing out-of-bounds at the end of the array
- Update max_people and max_period
Sorted:
timestamp  num_people  event_type       curr_people  max_people       max_period
1          2           EventType.ENTER  2            2                [1, 3]
3          1           EventType.ENTER  3            2 (not updated)  [1, 3]
3          2           EventType.EXIT   1            2                [3, 7]
7          3           EventType.ENTER  4            4                [7, 8]
8          2           EventType.EXIT   2            4                [7, 8]
9          2           EventType.EXIT   0            4                [7, 8]
Complexity:
- Time: O(nlog(n)) for the sort
- Space: O(1), assuming the sort is in-place
Code
from enum import Enum
​
​
class Data(object):
​
    def __init__(self, timestamp, num_people, event_type):
        self.timestamp = timestamp
        self.num_people = num_people
        self.event_type = event_type
​
    def __lt__(self, other):
        return self.timestamp < other.timestamp
​
​
class Period(object):
​
    def __init__(self, start, end):
        self.start = start
        self.end = end
​
    def __eq__(self, other):
        return self.start == other.start and self.end == other.end
​
    def __repr__(self):
        return str(self.start) + ', ' + str(self.end)
​
​
class EventType(Enum):
​
    ENTER = 0
    EXIT = 1
class Solution(object):
​
    def find_busiest_period(self, data):
        if data is None:
            raise TypeError('data cannot be None')
        if not data:
            return None
        data.sort()
        max_period = Period(0, 0)
        max_people = 0
        curr_people = 0
        for index, interval in enumerate(data):
            if interval.event_type == EventType.ENTER:
                curr_people += interval.num_people
            elif interval.event_type == EventType.EXIT:
                curr_people -= interval.num_people
            else:
                raise ValueError('Invalid event type')
            if (index < len(data) - 1 and
                    data[index].timestamp == data[index + 1].timestamp):
                continue
            if curr_people > max_people:
                max_people = curr_people
                max_period.start = data[index].timestamp
                if index < len(data) - 1:
                    max_period.end = data[index + 1].timestamp
                else:
                    max_period.end = data[index].timestamp
        return max_period
Unit Test
%%writefile test_find_busiest_period.py
from nose.tools import assert_equal, assert_raises
​
​
class TestSolution(object):
​
    def test_find_busiest_period(self):
        solution = Solution()
        assert_raises(TypeError, solution.find_busiest_period, None)
        assert_equal(solution.find_busiest_period([]), None)
        data = [
            Data(3, 2, EventType.EXIT),
            Data(1, 2, EventType.ENTER),
            Data(3, 1, EventType.ENTER),
            Data(7, 3, EventType.ENTER),
            Data(9, 2, EventType.EXIT),
            Data(8, 2, EventType.EXIT),
        ]
        assert_equal(solution.find_busiest_period(data), Period(7, 8))
        print('Success: test_find_busiest_period')
​
​
def main():
    test = TestSolution()
    test.test_find_busiest_period()
​
​
if __name__ == '__main__':
    main()

Q: Problem: Implement a min heap with extract_min and insert methods.
A: Constraints
- Can we assume the inputs are ints?
  - Yes
- Can we assume this fits memory?
  - Yes
Test Cases
- Extract min of an empty tree
- Extract min general case
- Insert into an empty tree
- Insert general case (left and right insert)
          _5_
        /     \
       20     15
      / \    /  \
     22  40 25
extract_min(): 5
          _15_
        /      \
       20      25
      / \     /  \
     22  40
insert(2):
          _2_
        /     \
       20      5
      / \     / \
     22  40  25  15
Algorithm
A heap is a complete binary tree where each node is smaller than its children.
extract_min
          _5_
        /     \
       20     15
      / \    /  \
     22  40 25
Save the root as the value to be returned: 5
Move the right most element to the root: 25
          _25_
        /      \
       20      15
      / \     /  \
     22  40
Bubble down 25: Swap 25 and 15 (the smaller child)
          _15_
        /      \
       20      25
      / \     /  \
     22  40
Return 5
We'll use an array to represent the tree, here are the indices:
          _0_
        /     \
       1       2
      / \     / \
     3   4
To get to a child, we take 2 index + 1 (left child) or 2 index + 2 (right child).
For example, the right child of index 1 is 2 * 1 + 2 = 4.
Complexity:
- Time: O(lg(n))
- Space: O(lg(n)) for the recursion depth (tree height), or O(1) if using an iterative approach
insert
          _5_
        /     \
       20     15
      / \    /  \
     22  40 25
insert(2):
Insert at the right-most spot to maintain the heap property.
          _5_
        /     \
       20     15
      / \    /  \
     22  40 25   2
Bubble up 2: Swap 2 and 15
          _5_
        /     \
       20     2
      / \    / \
     22  40 25  15
Bubble up 2: Swap 2 and 5
          _2_
        /     \
       20     5
      / \    / \
     22  40 25  15
We'll use an array to represent the tree, here are the indices:
          _0_
        /     \
       1       2
      / \     / \
     3   4   5   6
To get to a parent, we take (index - 1) // 2.
For example, the parent of index 6 is (6 - 1) // 2 = 2.
Complexity:
- Time: O(lg(n))
- Space: O(lg(n)) for the recursion depth (tree height), or O(1) if using an iterative approach
Code
%%writefile min_heap.py
from __future__ import division
​
import sys
​
​
class MinHeap(object):
​
    def __init__(self):
        self.array = []
​
    def __len__(self):
        return len(self.array)
​
    def extract_min(self):
        if not self.array:
            return None
        if len(self.array) == 1:
            return self.array.pop(0)
        minimum = self.array[0]
        # Move the last element to the root
        self.array[0] = self.array.pop(-1)
        self._bubble_down(index=0)
        return minimum
​
    def peek_min(self):
        return self.array[0] if self.array else None
​
    def insert(self, key):
        if key is None:
            raise TypeError('key cannot be None')
        self.array.append(key)
        self._bubble_up(index=len(self.array) - 1)
​
    def _bubble_up(self, index):
        if index == 0:
            return
        index_parent = (index - 1) // 2
        if self.array[index] < self.array[index_parent]:
            # Swap the indices and recurse
            self.array[index], self.array[index_parent] = \
                self.array[index_parent], self.array[index]
            self._bubble_up(index_parent)
​
    def _bubble_down(self, index):
        min_child_index = self._find_smaller_child(index)
        if min_child_index == -1:
            return
        if self.array[index] > self.array[min_child_index]:
            # Swap the indices and recurse
            self.array[index], self.array[min_child_index] = \
                self.array[min_child_index], self.array[index]
            self._bubble_down(min_child_index)
​
    def _find_smaller_child(self, index):
        left_child_index = 2 * index + 1
        right_child_index = 2 * index + 2
        # No right child
        if right_child_index >= len(self.array):
            # No left child
            if left_child_index >= len(self.array):
                return -1
            # Left child only
            else:
                return left_child_index
        else:
            # Compare left and right children
            if self.array[left_child_index] < self.array[right_child_index]:
                return left_child_index
            else:
                return right_child_index
Unit Test
%%writefile test_min_heap.py
from nose.tools import assert_equal
​
​
class TestMinHeap(object):
​
    def test_min_heap(self):
        heap = MinHeap()
        assert_equal(heap.peek_min(), None)
        assert_equal(heap.extract_min(), None)
        heap.insert(20)
        assert_equal(heap.array[0], 20)
        heap.insert(5)
        assert_equal(heap.array[0], 5)
        assert_equal(heap.array[1], 20)
        heap.insert(15)
        assert_equal(heap.array[0], 5)
        assert_equal(heap.array[1], 20)
        assert_equal(heap.array[2], 15)
        heap.insert(22)
        assert_equal(heap.array[0], 5)
        assert_equal(heap.array[1], 20)
        assert_equal(heap.array[2], 15)
        assert_equal(heap.array[3], 22)
        heap.insert(40)
        assert_equal(heap.array[0], 5)
        assert_equal(heap.array[1], 20)
        assert_equal(heap.array[2], 15)
        assert_equal(heap.array[3], 22)
        assert_equal(heap.array[4], 40)
        heap.insert(3)
        assert_equal(heap.array[0], 3)
        assert_equal(heap.array[1], 20)
        assert_equal(heap.array[2], 5)
        assert_equal(heap.array[3], 22)
        assert_equal(heap.array[4], 40)
        assert_equal(heap.array[5], 15)
        mins = []
        while heap:
            mins.append(heap.extract_min())
        assert_equal(mins, [3, 5, 15, 20, 22, 40])
        print('Success: test_min_heap')
​
def main():
    test = TestMinHeap()
    test.test_min_heap()
​
if __name__ == '__main__':
    main()