|
1 | | -from typing import List |
| 1 | +""" |
| 2 | +A pure Python implementation of library sort (also called gapped insertion sort). |
2 | 3 |
|
| 4 | +It works like insertion sort, but leaves empty gaps in the array so a new item can |
| 5 | +be placed without shifting everything after it. When the gaps fill up, the array is |
| 6 | +rebalanced. Expected time is O(n log n). |
3 | 7 |
|
4 | | -def library_sort(collection: List[int], reverse: bool = False) -> List[int]: |
| 8 | +Reference: https://en.wikipedia.org/wiki/Library_sort |
| 9 | +
|
| 10 | +For doctests run the following command: |
| 11 | +python3 -m doctest -v library_sort.py |
| 12 | +
|
| 13 | +For manual testing run: |
| 14 | +python3 library_sort.py |
| 15 | +""" |
| 16 | + |
| 17 | +_EMPTY = object() |
| 18 | + |
| 19 | + |
| 20 | +def _spread(slots: list) -> None: |
5 | 21 | """ |
6 | | - A pure Python implementation of library sort (also called gapped insertion sort). |
| 22 | + Redistribute the stored items evenly across all slots, keeping their order. |
7 | 23 |
|
8 | | - It w orks like insertion sort, but leaves empty gaps in the array so a new item can |
9 | | - be placed without shifting everything after it. When the gaps fill up, the array is |
10 | | - rebalanced. Expected time is O(n log n). |
| 24 | + >>> slots = [3, 5, 7, _EMPTY, _EMPTY, _EMPTY] |
| 25 | + >>> _spread(slots) |
| 26 | + >>> [x if x is not _EMPTY else None for x in slots] |
| 27 | + [3, None, 5, None, 7, None] |
| 28 | + """ |
| 29 | + items = [x for x in slots if x is not _EMPTY] |
| 30 | + for i in range(len(slots)): |
| 31 | + slots[i] = _EMPTY |
| 32 | + for i, item in enumerate(items): |
| 33 | + slots[i * len(slots) // len(items)] = item |
11 | 34 |
|
12 | | - Reference: https://en.wikipedia.org/wiki/Library_sort |
13 | 35 |
|
14 | | - For doctests run the following command: |
15 | | - python3 -m doctest -v library_sort.py |
| 36 | +def _insert(slots: list, item) -> None: |
| 37 | + """ |
| 38 | + Insert item into the gapped array, keeping the stored items in sorted order. |
| 39 | +
|
| 40 | + >>> slots = [1, _EMPTY, 5, _EMPTY] |
| 41 | + >>> _insert(slots, 3) |
| 42 | + >>> [x if x is not _EMPTY else None for x in slots] |
| 43 | + [1, 3, 5, None] |
| 44 | + """ |
| 45 | + size = len(slots) |
| 46 | + low, high = 0, size - 1 |
| 47 | + # Binary search over the slots, skipping empty ones. |
| 48 | + while low <= high: |
| 49 | + mid = (low + high) // 2 |
| 50 | + probe = mid |
| 51 | + while probe <= high and slots[probe] is _EMPTY: |
| 52 | + probe += 1 |
| 53 | + if probe > high: |
| 54 | + high = mid - 1 |
| 55 | + elif slots[probe] <= item: |
| 56 | + low = probe + 1 |
| 57 | + else: |
| 58 | + high = mid - 1 |
| 59 | + |
| 60 | + # Every stored item before `low` is <= item, every one from `low` on is > item. |
| 61 | + if low < size and slots[low] is _EMPTY: |
| 62 | + slots[low] = item |
| 63 | + return |
| 64 | + |
| 65 | + right = low |
| 66 | + while right < size and slots[right] is not _EMPTY: |
| 67 | + right += 1 |
| 68 | + left = low - 1 |
| 69 | + while left >= 0 and slots[left] is not _EMPTY: |
| 70 | + left -= 1 |
| 71 | + |
| 72 | + # Shift items towards the nearest gap to make room. |
| 73 | + if left < 0 or (right < size and right - low <= low - 1 - left): |
| 74 | + slots[low + 1 : right + 1] = slots[low:right] |
| 75 | + slots[low] = item |
| 76 | + else: |
| 77 | + slots[left : low - 1] = slots[left + 1 : low] |
| 78 | + slots[low - 1] = item |
| 79 | + |
| 80 | + |
| 81 | +def library_sort(collection: list) -> list: |
| 82 | + """ |
| 83 | + A pure Python implementation of library sort (also called gapped insertion sort). |
| 84 | +
|
| 85 | + :param collection: a collection of comparable items |
| 86 | + :return: a new list with the items in ascending order |
16 | 87 |
|
17 | | - For manual testing run: |
18 | | - python3 library_sort.py |
19 | | - Time Complexity: O(N log N) average, O(N^2) worst-case |
20 | | - Space Complexity: O(N) |
| 88 | + Time complexity: O(n log n) on average, O(n^2) in the worst case. |
| 89 | + Space complexity: O(n) |
21 | 90 |
|
22 | 91 | Examples: |
23 | 92 | >>> library_sort([5, 2, 9, 1, 5, 6]) |
24 | 93 | [1, 2, 5, 5, 6, 9] |
25 | | - >>> library_sort([3, 1, 4, 1, 5, 9, 2]) |
26 | | - [1, 1, 2, 3, 4, 5, 9] |
27 | 94 | >>> library_sort([]) |
28 | 95 | [] |
29 | 96 | >>> library_sort([1]) |
30 | 97 | [1] |
31 | | - >>> library_sort([5, 2, 9, 1], reverse=True) |
32 | | - [9, 5, 2, 1] |
| 98 | + >>> library_sort([-2, 5, 0, -45]) |
| 99 | + [-45, -2, 0, 5] |
| 100 | + >>> library_sort(["d", "a", "c", "b"]) |
| 101 | + ['a', 'b', 'c', 'd'] |
| 102 | + >>> import random |
| 103 | + >>> data = [random.randint(-100, 100) for _ in range(200)] |
| 104 | + >>> library_sort(data) == sorted(data) |
| 105 | + True |
33 | 106 | """ |
34 | | - if not collection: |
35 | | - return [] |
36 | | - |
37 | 107 | n = len(collection) |
38 | | - gapped = [None] * (2 * n) |
39 | | - gapped[0] = collection[0] |
40 | | - |
41 | | - for i in range(1, n): |
42 | | - val = collection[i] |
43 | | - valid_indices = [idx for idx, x in enumerate(gapped) if x is not None] |
44 | | - valid_vals = [gapped[idx] for idx in valid_indices] |
45 | | - |
46 | | - low, high = 0, len(valid_vals) - 1 |
47 | | - pos = len(valid_vals) |
48 | | - while low <= high: |
49 | | - mid = (low + high) // 2 |
50 | | - if valid_vals[mid] >= val: |
51 | | - pos = mid |
52 | | - high = mid - 1 |
53 | | - else: |
54 | | - low = mid + 1 |
55 | | - |
56 | | - if pos == len(valid_vals): |
57 | | - target_idx = valid_indices[-1] + 1 if valid_indices else 0 |
58 | | - else: |
59 | | - target_idx = valid_indices[pos] |
| 108 | + if n < 2: |
| 109 | + return list(collection) |
60 | 110 |
|
61 | | - if target_idx < len(gapped) and gapped[target_idx] is None: |
62 | | - gapped[target_idx] = val |
63 | | - else: |
64 | | - current_elements = [x for x in gapped if x is not None] |
65 | | - current_elements.append(val) |
66 | | - current_elements.sort() |
67 | | - |
68 | | - gapped = [None] * (2 * len(current_elements)) |
69 | | - step = len(gapped) // len(current_elements) |
70 | | - for idx, elem in enumerate(current_elements): |
71 | | - gapped[idx * step] = elem |
72 | | - |
73 | | - sorted_arr = [x for x in gapped if x is not None] |
74 | | - if reverse: |
75 | | - sorted_arr.reverse() |
76 | | - return sorted_arr |
| 111 | + slots: list = [_EMPTY] * (2 * n) |
| 112 | + next_spread = 1 |
| 113 | + for count, item in enumerate(collection, start=1): |
| 114 | + _insert(slots, item) |
| 115 | + if count == next_spread: |
| 116 | + _spread(slots) |
| 117 | + next_spread *= 2 |
| 118 | + return [x for x in slots if x is not _EMPTY] |
77 | 119 |
|
78 | 120 |
|
79 | 121 | if __name__ == "__main__": |
80 | | - import doctest |
81 | | - |
82 | | - doctest.testmod() |
83 | | - |
84 | | - print("\nLibrarySort Interactive Testing") |
85 | | - print("=" * 40) |
86 | | - |
87 | | - try: |
88 | | - user_input = input("Enter numbers separated by a comma:\n").strip() |
89 | | - if user_input == "": |
90 | | - unsorted = [] |
91 | | - else: |
92 | | - unsorted = [int(item.strip()) for item in user_input.split(",")] |
93 | | - |
94 | | - print(f"\nOriginal: {unsorted}") |
95 | | - sorted_list = library_sort(unsorted) |
96 | | - print(f"Sorted: {sorted_list}") |
97 | | - |
98 | | - # Test reverse |
99 | | - sorted_reverse = library_sort(unsorted, reverse=True) |
100 | | - print(f"Reverse: {sorted_reverse}") |
101 | | - |
102 | | - except ValueError: |
103 | | - print("Invalid input. Please enter valid integers separated by commas.") |
104 | | - except KeyboardInterrupt: |
105 | | - print("\n\nGoodbye!") |
| 122 | + user_input = input("Enter numbers separated by a comma:\n").strip() |
| 123 | + unsorted = [int(item) for item in user_input.split(",")] |
| 124 | + print(library_sort(unsorted)) |
0 commit comments