| ... | ... | @@ -35,6 +35,15 @@ const Hint = enum { |
| 35 | 35 | unknown, |
| 36 | 36 | }; |
| 37 | 37 | |
| 38 | const Range = struct { |
| 39 | a: usize, |
| 40 | b: usize, |
| 41 | limit: usize, |
| 42 | leftmost: bool, |
| 43 | balanced: bool, |
| 44 | partitioned: bool, |
| 45 | }; |
| 46 | |
| 38 | 47 | /// Unstable in-place sort. O(n) best case, O(n*log(n)) worst case and average case. |
| 39 | 48 | /// O(log(n)) memory (no allocator required). |
| 40 | 49 | /// `context` must have methods `swap` and `lessThan`, |
| ... | ... | @@ -46,16 +55,19 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void { |
| 46 | 55 | // number of allowed imbalanced partitions before switching to heap sort. |
| 47 | 56 | const max_limit = if (b > a) math.log2_int(usize, b - a) else 0; |
| 48 | 57 | |
| 49 | | // set upper bound on stack memory usage. |
| 50 | | const Range = struct { a: usize, b: usize, limit: usize, leftmost: bool }; |
| 51 | | var stack: [2 * @bitSizeOf(usize)]Range = undefined; |
| 52 | | var range = Range{ .a = a, .b = b, .limit = max_limit, .leftmost = true }; |
| 58 | // stack usage is bounded by log_2(n) due to placing longer partition onto stack each iteration. |
| 59 | var stack: [@bitSizeOf(usize)]Range = undefined; |
| 60 | var range = Range{ |
| 61 | .a = a, |
| 62 | .b = b, |
| 63 | .limit = max_limit, |
| 64 | .leftmost = true, |
| 65 | .balanced = true, |
| 66 | .partitioned = true, |
| 67 | }; |
| 53 | 68 | var top: usize = 0; |
| 54 | 69 | |
| 55 | 70 | while (true) { |
| 56 | | var was_balanced = true; |
| 57 | | var was_partitioned = true; |
| 58 | | |
| 59 | 71 | while (true) { |
| 60 | 72 | const len = range.b - range.a; |
| 61 | 73 | |
| ... | ... | @@ -76,7 +88,7 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void { |
| 76 | 88 | |
| 77 | 89 | // if the last partitioning was imbalanced, try breaking patterns in the slice by shuffling |
| 78 | 90 | // some elements around. Hopefully we'll choose a better pivot this time. |
| 79 | | if (!was_balanced) { |
| 91 | if (!range.balanced) { |
| 80 | 92 | breakPatterns(range.a, range.b, context); |
| 81 | 93 | range.limit -= 1; |
| 82 | 94 | } |
| ... | ... | @@ -95,7 +107,7 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void { |
| 95 | 107 | |
| 96 | 108 | // if the last partitioning was decently balanced and didn't shuffle elements, and if pivot |
| 97 | 109 | // selection predicts the slice is likely already sorted... |
| 98 | | if (was_balanced and was_partitioned and hint == .increasing) { |
| 110 | if (range.balanced and range.partitioned and hint == .increasing) { |
| 99 | 111 | // try identifying several out-of-order elements and shifting them to correct |
| 100 | 112 | // positions. If the slice ends up being completely sorted, we're done. |
| 101 | 113 | if (partialInsertionSort(range.a, range.b, context)) break; |
| ... | ... | @@ -111,23 +123,45 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void { |
| 111 | 123 | |
| 112 | 124 | // partition the slice. |
| 113 | 125 | var mid = pivot; |
| 114 | | was_partitioned = partition(range.a, range.b, &mid, context); |
| 126 | const was_partitioned = partition(range.a, range.b, &mid, context); |
| 115 | 127 | |
| 116 | 128 | const left_len = mid - range.a; |
| 117 | | const right_len = range.b - mid; |
| 129 | const right_len = range.b - (mid + 1); |
| 118 | 130 | const balanced_threshold = len / 8; |
| 119 | | if (left_len < right_len) { |
| 120 | | was_balanced = left_len >= balanced_threshold; |
| 121 | | stack[top] = .{ .a = range.a, .b = mid, .limit = range.limit, .leftmost = range.leftmost }; |
| 122 | | top += 1; |
| 123 | | range.a = mid + 1; |
| 124 | | range.leftmost = false; |
| 125 | | } else { |
| 126 | | was_balanced = right_len >= balanced_threshold; |
| 127 | | stack[top] = .{ .a = mid + 1, .b = range.b, .limit = range.limit, .leftmost = false }; |
| 128 | | top += 1; |
| 129 | | range.b = mid; |
| 130 | | } |
| 131 | |
| 132 | const left_is_smaller = left_len < right_len; |
| 133 | |
| 134 | const smaller_len = if (left_is_smaller) left_len else right_len; |
| 135 | const was_balanced = smaller_len >= balanced_threshold; |
| 136 | |
| 137 | const smaller_start_offset = if (left_is_smaller) range.a else mid + 1; |
| 138 | const larger_start_offset = if (left_is_smaller) mid + 1 else range.a; |
| 139 | const smaller_end_exclusive_offset = if (left_is_smaller) mid else range.b; |
| 140 | const larger_end_exclusive_offset = if (left_is_smaller) range.b else mid; |
| 141 | |
| 142 | const smaller_is_leftmost = if (left_is_smaller) range.leftmost else false; |
| 143 | const larger_is_leftmost = if (left_is_smaller) false else range.leftmost; |
| 144 | |
| 145 | // defer sorting the larger range until later to ensure stack usage is always less than log_2(n): |
| 146 | // as if we always push more than half the range to the stack then each time we push to the stack |
| 147 | // we reduce the amount of items we can push to it in later iterations by at least n/2 |
| 148 | // therefore the count of items on the stack can never be more than log_2(n) |
| 149 | stack[top] = .{ |
| 150 | .a = larger_start_offset, |
| 151 | .b = larger_end_exclusive_offset, |
| 152 | .limit = range.limit, |
| 153 | .leftmost = larger_is_leftmost, |
| 154 | .balanced = was_balanced, |
| 155 | .partitioned = was_partitioned, |
| 156 | }; |
| 157 | top += 1; |
| 158 | |
| 159 | // sort the smaller range immediately |
| 160 | range.a = smaller_start_offset; |
| 161 | range.b = smaller_end_exclusive_offset; |
| 162 | range.leftmost = smaller_is_leftmost; |
| 163 | range.balanced = true; // this either already true, or the range is small so we don't care |
| 164 | range.partitioned = was_partitioned; |
| 131 | 165 | } |
| 132 | 166 | |
| 133 | 167 | top = math.sub(usize, top, 1) catch break; |