diff --git a/dotfiles/lib/python/rotated_array.py b/dotfiles/lib/python/rotated_array.py new file mode 100644 index 00000000..21af5a9e --- /dev/null +++ b/dotfiles/lib/python/rotated_array.py @@ -0,0 +1,91 @@ +def rotate_array(incoming, rotation_index): + new_back = incoming[:rotation_index] + new_front = incoming[rotation_index:] + new_front.extend(new_back) + return new_front + + +def binary_search( + array, item, low=0, high=None, + lower_predicate=lambda item, array, index, low, high: item <= array[index] +): + if low < 0: + raise ValueError('lo must be non-negative') + if high is None: + high = len(array) + while low < high: + mid = (low + high)//2 + if lower_predicate(item, array, mid, low, high): + high = mid + else: + low = mid + 1 + return low + + +class RotatedArrayProxy(object): + + def __init__(self, incoming): + self.incoming = incoming + self._rotation_index = None + if incoming: + # Duplicates can not span the rotation + assert incoming[0] != incoming[-1] + + def __getitem__(self, item): + if not isinstance(item, slice): + return self.incoming[self._actual_index(item)] + else: + self._handle_slice(item) + + def _handle_slice(self, item): + start = self._actual_index(item.start) + stop = self._actual_index(item.stop) + if start > stop: + sliced = self.incoming[start::item.stride] + # Ensure that the stride is preserved as it passes through + # the rotation. + start_index = (len(self.incoming) - start) % (item.stride or 1) + if start_index <= stop: + sliced.extend( + self.incoming[start_index:stop:item.stride] + ) + return sliced + else: + return self.incoming[start:stop:item.stride] + + def _actual_index(self, index): + if index is None: + return index + elif 0 <= index < len(self.incoming): + return (index + self.rotation_index) % len(self.incoming) + elif index == len(self.incoming): + return self.rotation_index + else: + raise Exception() + + @property + def rotation_index(self): + if self._rotation_index is None: + self._rotation_index = self._find_rotation_index() + return self._rotation_index + + def _find_lower_predicate(self, item, array, index, low, high): + return array[0] > array[index] + + def _find_rotation_index(self): + if len(self.incoming) < 1: + return 0 + return binary_search(self.incoming, self.incoming[0], + lower_predicate=self._find_lower_predicate) + + def __len__(self): + return len(self.incoming) + + def sorted_insertion_index(self, x): + return binary_search(self, x) + + def actual_insertion_index(self, x): + return self._actual_index(self.sorted_insertion_index(x)) + + def unrotated(self): + return rotate_array(self.incoming, self.rotation_index) diff --git a/dotfiles/lib/python/rotated_array_proxy_test.py b/dotfiles/lib/python/rotated_array_proxy_test.py new file mode 100644 index 00000000..dc86d379 --- /dev/null +++ b/dotfiles/lib/python/rotated_array_proxy_test.py @@ -0,0 +1,69 @@ +import rotated_array + +# duplicates, slicing with stride, insertion index for item greater than everything for completely sorted array +def test_empty_rotated_array_proxy(): + empty_rap = rotated_array.RotatedArrayProxy([]) + assert empty_rap.rotation_index == 0 + assert empty_rap.unrotated() == [] + assert empty_rap.sorted_insertion_index(100) == 0 + assert empty_rap.actual_insertion_index(100) == 0 + + +def test_inserting_at_end_of_insertion_range(): + rap = rotated_array.RotatedArrayProxy([3, 4, 5, 0, 2]) + assert rap.rotation_index == 3 + assert rap.unrotated() == [0, 2, 3, 4, 5] + + assert rap.sorted_insertion_index(-1) == 0 + assert rap.actual_insertion_index(-1) == 3 + + assert rap.sorted_insertion_index(1) == 1 + assert rap.actual_insertion_index(1) == 4 + + assert rap.sorted_insertion_index(2) in [1, 2] + assert rap.actual_insertion_index(2) in [4, 5] + + assert rap.sorted_insertion_index(3) in [2, 3] + assert rap.actual_insertion_index(3) in [0, 1] + + +def test_inserting_for_sorted_array(): + rap = rotated_array.RotatedArrayProxy([0, 1]) + assert rap.unrotated() == [0, 1] + assert rap.sorted_insertion_index(1000) == 2 + assert rap.actual_insertion_index(1000) == 2 + + +def test_inserting_largest_element(): + rap = rotated_array.RotatedArrayProxy([3, 0, 1]) + assert rap.rotation_index == 1 + assert rap.sorted_insertion_index(1000) == 3 + assert rap.actual_insertion_index(1000) == 1 + + +def test_inserting_largest_element(): + rap = rotated_array.RotatedArrayProxy([3, 0, 1]) + assert rap.unrotated() == [0, 1, 3] + assert rap.actual_insertion_index(2) == 0 + assert rap.actual_insertion_index(1) in [2, 3] + + +def test_rotation_index_and_unrotate(): + arr = [3]*117 + [1] + [2]*16 + rap = rotated_array.RotatedArrayProxy(arr) + assert rap[0] == 1 + assert rap.rotation_index == 117 + assert rap.unrotated() == sorted(arr) + + arr = [3, 3, 3, 3, 1, 1, 1, 2, 2] + rap = rotated_array.RotatedArrayProxy(arr) + assert rap.rotation_index == 4 + assert rap.unrotated() == sorted(arr) + + rap = rotated_array.RotatedArrayProxy([3, 3, 3, 3, 1, 1, 1, 2]) + assert rap.rotation_index == 4 + assert rap.unrotated() == [1, 1, 1, 2, 3, 3, 3, 3] + + +def test_insert(): +