| ... | @@ -35,6 +35,15 @@ const Hint = enum { | ... | @@ -35,6 +35,15 @@ const Hint = enum { |
| 35 | unknown, | 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 | /// Unstable in-place sort. O(n) best case, O(n*log(n)) worst case and average case. | 47 | /// Unstable in-place sort. O(n) best case, O(n*log(n)) worst case and average case. |
| 39 | /// O(log(n)) memory (no allocator required). | 48 | /// O(log(n)) memory (no allocator required). |
| 40 | /// `context` must have methods `swap` and `lessThan`, | 49 | /// `context` must have methods `swap` and `lessThan`, |
| ... | @@ -46,16 +55,19 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void { | ... | @@ -46,16 +55,19 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void { |
| 46 | // number of allowed imbalanced partitions before switching to heap sort. | 55 | // number of allowed imbalanced partitions before switching to heap sort. |
| 47 | const max_limit = if (b > a) math.log2_int(usize, b - a) else 0; | 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. | 58 | // stack usage is bounded by log_2(n) due to placing longer partition onto stack each iteration. |
| 50 | const Range = struct { a: usize, b: usize, limit: usize, leftmost: bool }; | 59 | var stack: [@bitSizeOf(usize)]Range = undefined; |
| 51 | var stack: [2 * @bitSizeOf(usize)]Range = undefined; | 60 | var range = Range{ |
| 52 | var range = Range{ .a = a, .b = b, .limit = max_limit, .leftmost = true }; | 61 | .a = a, |
| | 62 | .b = b, |
| | 63 | .limit = max_limit, |
| | 64 | .leftmost = true, |
| | 65 | .balanced = true, |
| | 66 | .partitioned = true, |
| | 67 | }; |
| 53 | var top: usize = 0; | 68 | var top: usize = 0; |
| 54 | | 69 | |
| 55 | while (true) { | 70 | while (true) { |
| 56 | var was_balanced = true; | | |
| 57 | var was_partitioned = true; | | |
| 58 | | | |
| 59 | while (true) { | 71 | while (true) { |
| 60 | const len = range.b - range.a; | 72 | const len = range.b - range.a; |
| 61 | | 73 | |
| ... | @@ -76,7 +88,7 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void { | ... | @@ -76,7 +88,7 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void { |
| 76 | | 88 | |
| 77 | // if the last partitioning was imbalanced, try breaking patterns in the slice by shuffling | 89 | // if the last partitioning was imbalanced, try breaking patterns in the slice by shuffling |
| 78 | // some elements around. Hopefully we'll choose a better pivot this time. | 90 | // some elements around. Hopefully we'll choose a better pivot this time. |
| 79 | if (!was_balanced) { | 91 | if (!range.balanced) { |
| 80 | breakPatterns(range.a, range.b, context); | 92 | breakPatterns(range.a, range.b, context); |
| 81 | range.limit -= 1; | 93 | range.limit -= 1; |
| 82 | } | 94 | } |
| ... | @@ -95,7 +107,7 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void { | ... | @@ -95,7 +107,7 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void { |
| 95 | | 107 | |
| 96 | // if the last partitioning was decently balanced and didn't shuffle elements, and if pivot | 108 | // if the last partitioning was decently balanced and didn't shuffle elements, and if pivot |
| 97 | // selection predicts the slice is likely already sorted... | 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 | // try identifying several out-of-order elements and shifting them to correct | 111 | // try identifying several out-of-order elements and shifting them to correct |
| 100 | // positions. If the slice ends up being completely sorted, we're done. | 112 | // positions. If the slice ends up being completely sorted, we're done. |
| 101 | if (partialInsertionSort(range.a, range.b, context)) break; | 113 | if (partialInsertionSort(range.a, range.b, context)) break; |
| ... | @@ -111,23 +123,45 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void { | ... | @@ -111,23 +123,45 @@ pub fn pdqContext(a: usize, b: usize, context: anytype) void { |
| 111 | | 123 | |
| 112 | // partition the slice. | 124 | // partition the slice. |
| 113 | var mid = pivot; | 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 | const left_len = mid - range.a; | 128 | const left_len = mid - range.a; |
| 117 | const right_len = range.b - mid; | 129 | const right_len = range.b - (mid + 1); |
| 118 | const balanced_threshold = len / 8; | 130 | const balanced_threshold = len / 8; |
| 119 | if (left_len < right_len) { | 131 | |
| 120 | was_balanced = left_len >= balanced_threshold; | 132 | const left_is_smaller = left_len < right_len; |
| 121 | stack[top] = .{ .a = range.a, .b = mid, .limit = range.limit, .leftmost = range.leftmost }; | 133 | |
| 122 | top += 1; | 134 | const smaller_len = if (left_is_smaller) left_len else right_len; |
| 123 | range.a = mid + 1; | 135 | const was_balanced = smaller_len >= balanced_threshold; |
| 124 | range.leftmost = false; | 136 | |
| 125 | } else { | 137 | const smaller_start_offset = if (left_is_smaller) range.a else mid + 1; |
| 126 | was_balanced = right_len >= balanced_threshold; | 138 | const larger_start_offset = if (left_is_smaller) mid + 1 else range.a; |
| 127 | stack[top] = .{ .a = mid + 1, .b = range.b, .limit = range.limit, .leftmost = false }; | 139 | const smaller_end_exclusive_offset = if (left_is_smaller) mid else range.b; |
| 128 | top += 1; | 140 | const larger_end_exclusive_offset = if (left_is_smaller) range.b else mid; |
| 129 | range.b = mid; | 141 | |
| 130 | } | 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 | top = math.sub(usize, top, 1) catch break; | 167 | top = math.sub(usize, top, 1) catch break; |