├── .gitignore ├── .gitattributes ├── media ├── logo.fig └── logo.png ├── src ├── bubble.zig ├── selection.zig ├── shell.zig ├── comb.zig ├── main.zig ├── quick.zig ├── merge.zig ├── twin.zig ├── tail.zig ├── test.zig ├── radix.zig └── tim.zig ├── LICENSE ├── bench ├── generator.zig └── bench.zig └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-out/ 3 | *.o 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.zig text eol=lf 3 | zigmod.* text eol=lf 4 | -------------------------------------------------------------------------------- /media/logo.fig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alichraghi/zort/HEAD/media/logo.fig -------------------------------------------------------------------------------- /media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alichraghi/zort/HEAD/media/logo.png -------------------------------------------------------------------------------- /src/bubble.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn bubbleSort( 4 | comptime T: type, 5 | arr: []T, 6 | context: anytype, 7 | comptime cmp: fn (context: @TypeOf(context), lhs: T, rhs: T) bool, 8 | ) void { 9 | for (0..arr.len) |i| { 10 | for (0..arr.len - i - 1) |j| { 11 | if (cmp(context, arr[j + 1], arr[j])) { 12 | std.mem.swap(T, &arr[j], &arr[j + 1]); 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/selection.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zort = @import("main.zig"); 3 | 4 | pub fn selectionSort( 5 | comptime T: type, 6 | arr: []T, 7 | context: anytype, 8 | comptime cmp: fn (context: @TypeOf(context), lhs: T, rhs: T) bool, 9 | ) void { 10 | for (arr, 0..) |*item, i| { 11 | var pos = i; 12 | var j = i + 1; 13 | while (j < arr.len) : (j += 1) { 14 | if (cmp(context, arr[j], arr[pos])) { 15 | pos = j; 16 | } 17 | } 18 | std.mem.swap(T, &arr[pos], item); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/shell.zig: -------------------------------------------------------------------------------- 1 | const zort = @import("main.zig"); 2 | 3 | pub fn shellSort( 4 | comptime T: type, 5 | arr: []T, 6 | context: anytype, 7 | comptime cmp: fn (context: @TypeOf(context), lhs: T, rhs: T) bool, 8 | ) void { 9 | var gap = arr.len / 2; 10 | while (gap > 0) : (gap /= 2) { 11 | var i = gap; 12 | while (i < arr.len) : (i += 1) { 13 | const x = arr[i]; 14 | var j = i; 15 | while (j >= gap and cmp(context, x, arr[j - gap])) : (j -= gap) { 16 | arr[j] = arr[j - gap]; 17 | } 18 | arr[j] = x; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/comb.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zort = @import("main.zig"); 3 | 4 | pub fn combSort( 5 | comptime T: type, 6 | arr: []T, 7 | context: anytype, 8 | comptime cmp: fn (context: @TypeOf(context), lhs: T, rhs: T) bool, 9 | ) void { 10 | if (arr.len == 0) return; 11 | var gap = arr.len; 12 | var swapped = true; 13 | while (gap != 1 or swapped) { 14 | gap = (gap * 10 / 13) ^ 1; 15 | swapped = false; 16 | for (0..arr.len - gap) |i| { 17 | if (cmp(context, arr[i + gap], arr[i])) { 18 | std.mem.swap(T, &arr[i], &arr[i + gap]); 19 | swapped = true; 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | pub const bubbleSort = @import("bubble.zig").bubbleSort; 2 | pub const combSort = @import("comb.zig").combSort; 3 | pub const merge = @import("merge.zig").merge; 4 | pub const mergeSort = @import("merge.zig").mergeSort; 5 | pub const mergeSortAdvanced = @import("merge.zig").mergeSortAdvanced; 6 | pub const SortOptions = @import("radix.zig").SortOptions; 7 | pub const radixSort = @import("radix.zig").radixSort; 8 | pub const quickSort = @import("quick.zig").quickSort; 9 | pub const quickSortAdvanced = @import("quick.zig").quickSortAdvanced; 10 | pub const selectionSort = @import("selection.zig").selectionSort; 11 | pub const shellSort = @import("shell.zig").shellSort; 12 | pub const timSort = @import("tim.zig").timSort; 13 | pub const tailMerge = @import("tail.zig").tailMerge; 14 | pub const tailSort = @import("tail.zig").tailSort; 15 | pub const twinSort = @import("twin.zig").twinSort; 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ali Chraghi, Voroskoi and Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/quick.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zort = @import("main.zig"); 3 | 4 | pub fn quickSort( 5 | comptime T: type, 6 | arr: []T, 7 | context: anytype, 8 | comptime cmp: fn (context: @TypeOf(context), lhs: T, rhs: T) bool, 9 | ) void { 10 | return quickSortAdvanced(T, arr, 0, @max(arr.len, 1) - 1, context, cmp); 11 | } 12 | 13 | pub fn quickSortAdvanced( 14 | comptime T: type, 15 | arr: []T, 16 | left: usize, 17 | right: usize, 18 | context: anytype, 19 | comptime cmp: fn (context: @TypeOf(context), lhs: T, rhs: T) bool, 20 | ) void { 21 | if (left >= right) return; 22 | const pivot = getPivot(T, arr, left, right, context, cmp); 23 | var i = left; 24 | var j = left; 25 | while (j < right) : (j += 1) { 26 | if (cmp(context, arr[j], pivot)) { 27 | std.mem.swap(T, &arr[i], &arr[j]); 28 | i += 1; 29 | } 30 | } 31 | std.mem.swap(T, &arr[i], &arr[right]); 32 | quickSortAdvanced(T, arr, left, @max(i, 1) - 1, context, cmp); 33 | quickSortAdvanced(T, arr, i + 1, right, context, cmp); 34 | } 35 | 36 | fn getPivot( 37 | comptime T: type, 38 | arr: []T, 39 | left: usize, 40 | right: usize, 41 | context: anytype, 42 | comptime cmp: fn (context: @TypeOf(context), lhs: T, rhs: T) bool, 43 | ) T { 44 | const mid = (left + right) / 2; 45 | if (cmp(context, arr[mid], arr[left])) std.mem.swap(T, &arr[mid], &arr[left]); 46 | if (cmp(context, arr[right], arr[left])) std.mem.swap(T, &arr[right], &arr[left]); 47 | if (cmp(context, arr[mid], arr[right])) std.mem.swap(T, &arr[mid], &arr[right]); 48 | return arr[right]; 49 | } 50 | -------------------------------------------------------------------------------- /bench/generator.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// Generate `limit` number or random items 4 | pub fn random(comptime T: type, allocator: std.mem.Allocator, limit: usize) std.mem.Allocator.Error![]T { 5 | var rnd = std.Random.DefaultPrng.init(@intCast(std.time.milliTimestamp())); 6 | 7 | var array = try std.ArrayList(T).initCapacity(allocator, limit); 8 | 9 | switch (@typeInfo(T)) { 10 | .int => { 11 | var i: usize = 0; 12 | while (i < limit) : (i += 1) { 13 | const item: T = rnd.random() 14 | .intRangeAtMostBiased(T, std.math.minInt(T), @as(T, @intCast(limit))); 15 | array.appendAssumeCapacity(item); 16 | } 17 | }, 18 | else => unreachable, 19 | } 20 | 21 | return array.toOwnedSlice(); 22 | } 23 | 24 | pub fn sorted(comptime T: type, allocator: std.mem.Allocator, limit: usize) std.mem.Allocator.Error![]T { 25 | const ret = try random(T, allocator, limit); 26 | 27 | std.mem.sort(T, ret, {}, comptime std.sort.asc(T)); 28 | 29 | return ret; 30 | } 31 | 32 | pub fn reverse(comptime T: type, allocator: std.mem.Allocator, limit: usize) std.mem.Allocator.Error![]T { 33 | const ret = try random(T, allocator, limit); 34 | 35 | std.mem.sort(T, ret, {}, comptime std.sort.desc(T)); 36 | 37 | return ret; 38 | } 39 | 40 | pub fn ascSaw(comptime T: type, allocator: std.mem.Allocator, limit: usize) std.mem.Allocator.Error![]T { 41 | const TEETH = 10; 42 | var ret = try random(T, allocator, limit); 43 | 44 | var offset: usize = 0; 45 | while (offset < TEETH) : (offset += 1) { 46 | const start = ret.len / TEETH * offset; 47 | std.mem.sort(T, ret[start .. start + ret.len / TEETH], {}, comptime std.sort.asc(T)); 48 | } 49 | 50 | return ret; 51 | } 52 | 53 | pub fn descSaw(comptime T: type, allocator: std.mem.Allocator, limit: usize) std.mem.Allocator.Error![]T { 54 | const TEETH = 10; 55 | var ret = try random(T, allocator, limit); 56 | 57 | var offset: usize = 0; 58 | while (offset < TEETH) : (offset += 1) { 59 | const start = ret.len / TEETH * offset; 60 | std.mem.sort(T, ret[start .. start + ret.len / TEETH], {}, comptime std.sort.desc(T)); 61 | } 62 | 63 | return ret; 64 | } 65 | -------------------------------------------------------------------------------- /src/merge.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zort = @import("main.zig"); 3 | 4 | pub fn merge( 5 | comptime T: type, 6 | allocator: std.mem.Allocator, 7 | arr: []T, 8 | left: usize, 9 | mid: usize, 10 | right: usize, 11 | context: anytype, 12 | comptime cmp: fn (context: @TypeOf(context), lhs: T, rhs: T) bool, 13 | ) std.mem.Allocator.Error!void { 14 | const n1 = mid - left + 1; 15 | const n2 = right - mid; 16 | 17 | var L = try allocator.alloc(T, n1); 18 | var R = try allocator.alloc(T, n2); 19 | defer { 20 | allocator.free(L); 21 | allocator.free(R); 22 | } 23 | 24 | var i: usize = 0; 25 | var j: usize = 0; 26 | 27 | while (i < n1) : (i += 1) { 28 | L[i] = arr[left + i]; 29 | } 30 | 31 | i = 0; 32 | while (i < n2) : (i += 1) { 33 | R[i] = arr[mid + 1 + i]; 34 | } 35 | 36 | i = 0; 37 | var k = left; 38 | while (i < n1 and j < n2) : (k += 1) { 39 | if (cmp(context, L[i], R[j])) { 40 | arr[k] = L[i]; 41 | i += 1; 42 | } else { 43 | arr[k] = R[j]; 44 | j += 1; 45 | } 46 | } 47 | 48 | while (i < n1) { 49 | arr[k] = L[i]; 50 | i += 1; 51 | k += 1; 52 | } 53 | 54 | while (j < n2) { 55 | arr[k] = R[j]; 56 | j += 1; 57 | k += 1; 58 | } 59 | } 60 | 61 | pub fn mergeSortAdvanced( 62 | comptime T: type, 63 | allocator: std.mem.Allocator, 64 | arr: []T, 65 | left: usize, 66 | right: usize, 67 | context: anytype, 68 | comptime cmp: fn (context: @TypeOf(context), lhs: T, rhs: T) bool, 69 | ) std.mem.Allocator.Error!void { 70 | if (left < right) { 71 | const mid = left + (right - left) / 2; 72 | 73 | try mergeSortAdvanced(T, allocator, arr, left, mid, context, cmp); 74 | try mergeSortAdvanced(T, allocator, arr, mid + 1, right, context, cmp); 75 | 76 | try merge(T, allocator, arr, left, mid, right, context, cmp); 77 | } 78 | } 79 | 80 | pub fn mergeSort( 81 | comptime T: type, 82 | allocator: std.mem.Allocator, 83 | arr: []T, 84 | context: anytype, 85 | comptime cmp: fn (context: @TypeOf(context), lhs: T, rhs: T) bool, 86 | ) std.mem.Allocator.Error!void { 87 | return mergeSortAdvanced(T, allocator, arr, 0, @max(arr.len, 1) - 1, context, cmp); 88 | } 89 | -------------------------------------------------------------------------------- /src/twin.zig: -------------------------------------------------------------------------------- 1 | // Zig port of twinsort by VÖRÖSKŐI András 2 | 3 | const std = @import("std"); 4 | const zort = @import("main.zig"); 5 | 6 | pub fn twinSort( 7 | comptime T: type, 8 | allocator: std.mem.Allocator, 9 | arr: []T, 10 | context: anytype, 11 | comptime cmp: fn (context: @TypeOf(context), lhs: T, rhs: T) bool, 12 | ) !void { 13 | if (twinSwap(T, arr, context, cmp) == 0) { 14 | try zort.tailMerge(T, allocator, arr, context, cmp, 2); 15 | } 16 | } 17 | 18 | /// Turn the array into sorted blocks of 2 elements. 19 | /// Detect and sort reverse order runs. So `6 5 4 3 2 1` becomes `1 2 3 4 5 6` 20 | /// rather than `5 6 3 4 1 2`. 21 | fn twinSwap( 22 | comptime T: type, 23 | arr: []T, 24 | context: anytype, 25 | comptime cmp: fn (context: @TypeOf(context), lhs: T, rhs: T) bool, 26 | ) usize { 27 | var index: usize = 0; 28 | var start: usize = undefined; 29 | var end: usize = arr.len - 2; 30 | while (index <= end) { 31 | if (cmp(context, arr[index], arr[index + 1])) { 32 | index += 2; 33 | continue; 34 | } 35 | 36 | start = index; 37 | index += 2; 38 | while (true) { 39 | if (index > end) { 40 | if (start == 0) { 41 | if (arr.len % 2 == 0 or arr[index - 1] > arr[index]) { 42 | // the entire slice was reversed 43 | end = arr.len - 1; 44 | while (start < end) { 45 | std.mem.swap(T, &arr[start], &arr[end]); 46 | start += 1; 47 | end -= 1; 48 | } 49 | return 1; 50 | } 51 | } 52 | break; 53 | } 54 | 55 | if (cmp(context, arr[index + 1], arr[index])) { 56 | if (cmp(context, arr[index], arr[index - 1])) { 57 | index += 2; 58 | continue; 59 | } 60 | std.mem.swap(T, &arr[index], &arr[index + 1]); 61 | } 62 | break; 63 | } 64 | 65 | end = index - 1; 66 | while (start < end) { 67 | std.mem.swap(T, &arr[start], &arr[end]); 68 | start += 1; 69 | end -= 1; 70 | } 71 | 72 | end = arr.len - 2; 73 | index += 2; 74 | } 75 | 76 | return 0; 77 | } 78 | -------------------------------------------------------------------------------- /src/tail.zig: -------------------------------------------------------------------------------- 1 | //! By VÖRÖSKŐI András 2 | 3 | const std = @import("std"); 4 | const zort = @import("main.zig"); 5 | 6 | pub fn tailSort( 7 | comptime T: type, 8 | allocator: std.mem.Allocator, 9 | arr: []T, 10 | context: anytype, 11 | comptime cmp: fn (context: @TypeOf(context), lhs: T, rhs: T) bool, 12 | ) !void { 13 | if (arr.len < 2) return; 14 | 15 | try tailMerge(T, allocator, arr, context, cmp, 1); 16 | } 17 | 18 | /// Bottom up merge sort. It copies the right block to swap, next merges 19 | /// starting at the tail ends of the two sorted blocks. 20 | /// Can be used stand alone. Uses at most arr.len / 2 swap memory. 21 | pub fn tailMerge( 22 | comptime T: type, 23 | allocator: std.mem.Allocator, 24 | arr: []T, 25 | context: anytype, 26 | comptime cmp: fn (context: @TypeOf(context), lhs: T, rhs: T) bool, 27 | b: u2, 28 | ) !void { 29 | var c: isize = undefined; 30 | var c_max: isize = undefined; 31 | var d: isize = undefined; 32 | var d_max: isize = undefined; 33 | var e: isize = undefined; 34 | 35 | var block: isize = b; 36 | 37 | var swap = try allocator.alloc(T, arr.len / 2); 38 | defer allocator.free(swap); 39 | 40 | while (block < arr.len) { 41 | var offset: isize = 0; 42 | while (offset + block < arr.len) : (offset += block * 2) { 43 | e = offset + block - 1; 44 | 45 | if (!cmp(context, arr[@as(usize, @intCast(e)) + 1], arr[@as(usize, @intCast(e))])) { 46 | continue; 47 | } 48 | 49 | if (offset + block * 2 < arr.len) { 50 | c_max = 0 + block; 51 | d_max = offset + block * 2; 52 | } else { 53 | c_max = 0 + @as(isize, @intCast(arr.len)) - (offset + block); 54 | d_max = 0 + @as(isize, @intCast(arr.len)); 55 | } 56 | 57 | d = d_max - 1; 58 | 59 | while (!cmp(context, arr[@as(usize, @intCast(d))], arr[@as(usize, @intCast(e))])) { 60 | d_max -= 1; 61 | d -= 1; 62 | c_max -= 1; 63 | } 64 | 65 | c = 0; 66 | d = offset + block; 67 | 68 | while (c < c_max) { 69 | swap[@as(usize, @intCast(c))] = arr[@as(usize, @intCast(d))]; 70 | c += 1; 71 | d += 1; 72 | } 73 | 74 | c -= 1; 75 | 76 | d = offset + block - 1; 77 | e = d_max - 1; 78 | 79 | if (!cmp( 80 | context, 81 | arr[@as(usize, @intCast(offset + block))], 82 | arr[@as(usize, @intCast(offset))], 83 | )) { 84 | arr[@as(usize, @intCast(e))] = arr[@as(usize, @intCast(d))]; 85 | e -= 1; 86 | d -= 1; 87 | while (c >= 0) { 88 | while (cmp( 89 | context, 90 | swap[@as(usize, @intCast(c))], 91 | arr[@as(usize, @intCast(d))], 92 | )) { 93 | arr[@as(usize, @intCast(e))] = arr[@as(usize, @intCast(d))]; 94 | e -= 1; 95 | d -= 1; 96 | } 97 | 98 | arr[@as(usize, @intCast(e))] = swap[@as(usize, @intCast(c))]; 99 | e -= 1; 100 | c -= 1; 101 | } 102 | } else { 103 | arr[@as(usize, @intCast(e))] = arr[@as(usize, @intCast(d))]; 104 | e -= 1; 105 | d -= 1; 106 | while (d >= offset) { 107 | while (!cmp( 108 | context, 109 | swap[@as(usize, @intCast(c))], 110 | arr[@as(usize, @intCast(d))], 111 | )) { 112 | arr[@as(usize, @intCast(e))] = swap[@as(usize, @intCast(c))]; 113 | e -= 1; 114 | c -= 1; 115 | } 116 | 117 | arr[@as(usize, @intCast(e))] = arr[@as(usize, @intCast(d))]; 118 | e -= 1; 119 | d -= 1; 120 | } 121 | while (c >= 0) { 122 | arr[@as(usize, @intCast(e))] = swap[@as(usize, @intCast(c))]; 123 | e -= 1; 124 | c -= 1; 125 | } 126 | } 127 | } 128 | block *= 2; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zort = @import("main.zig"); 3 | const testing = std.testing; 4 | 5 | pub const ItemsType = i32; 6 | pub const items = [_]i32{ -9, 1, -4, 12, 3, 4 }; 7 | pub const expectedASC = [_]i32{ -9, -4, 1, 3, 4, 12 }; 8 | pub const expectedDESC = [_]i32{ 12, 4, 3, 1, -4, -9 }; 9 | 10 | test "bubble" { 11 | { 12 | var arr = items; 13 | zort.bubbleSort(ItemsType, &arr, {}, comptime std.sort.asc(i32)); 14 | try testing.expectEqualSlices(ItemsType, &expectedASC, &arr); 15 | } 16 | { 17 | var arr = items; 18 | zort.bubbleSort(ItemsType, &arr, {}, comptime std.sort.desc(i32)); 19 | try testing.expectEqualSlices(ItemsType, &expectedDESC, &arr); 20 | } 21 | } 22 | 23 | test "comb" { 24 | { 25 | var arr = items; 26 | zort.combSort(ItemsType, &arr, {}, comptime std.sort.asc(i32)); 27 | try testing.expectEqualSlices(ItemsType, &expectedASC, &arr); 28 | } 29 | { 30 | var arr = items; 31 | zort.combSort(ItemsType, &arr, {}, comptime std.sort.desc(i32)); 32 | try testing.expectEqualSlices(ItemsType, &expectedDESC, &arr); 33 | } 34 | } 35 | 36 | test "merge" { 37 | { 38 | var arr = items; 39 | try zort.mergeSort(ItemsType, testing.allocator, &arr, {}, comptime std.sort.asc(i32)); 40 | try testing.expectEqualSlices(ItemsType, &expectedASC, &arr); 41 | } 42 | { 43 | var arr = items; 44 | try zort.mergeSort(ItemsType, testing.allocator, &arr, {}, comptime std.sort.desc(i32)); 45 | try testing.expectEqualSlices(ItemsType, &expectedDESC, &arr); 46 | } 47 | } 48 | 49 | test "quick" { 50 | { 51 | var arr = items; 52 | zort.quickSort(ItemsType, &arr, {}, comptime std.sort.asc(i32)); 53 | try testing.expectEqualSlices(ItemsType, &expectedASC, &arr); 54 | } 55 | { 56 | var arr = items; 57 | zort.quickSort(ItemsType, &arr, {}, comptime std.sort.desc(i32)); 58 | try testing.expectEqualSlices(ItemsType, &expectedDESC, &arr); 59 | } 60 | } 61 | 62 | test "radix" { 63 | { 64 | var arr = items; 65 | try zort.radixSort(ItemsType, .{}, testing.allocator, &arr); 66 | try testing.expectEqualSlices(ItemsType, &expectedASC, &arr); 67 | } 68 | } 69 | 70 | test "selection" { 71 | { 72 | var arr = items; 73 | zort.selectionSort(ItemsType, &arr, {}, comptime std.sort.asc(i32)); 74 | try testing.expectEqualSlices(ItemsType, &expectedASC, &arr); 75 | } 76 | { 77 | var arr = items; 78 | zort.selectionSort(ItemsType, &arr, {}, comptime std.sort.desc(i32)); 79 | try testing.expectEqualSlices(ItemsType, &expectedDESC, &arr); 80 | } 81 | } 82 | 83 | test "shell" { 84 | { 85 | var arr = items; 86 | zort.shellSort(ItemsType, &arr, {}, comptime std.sort.asc(i32)); 87 | try testing.expectEqualSlices(ItemsType, &expectedASC, &arr); 88 | } 89 | { 90 | var arr = items; 91 | zort.shellSort(ItemsType, &arr, {}, comptime std.sort.desc(i32)); 92 | try testing.expectEqualSlices(ItemsType, &expectedDESC, &arr); 93 | } 94 | } 95 | 96 | test "tim" { 97 | { 98 | var arr = items; 99 | try zort.timSort(ItemsType, testing.allocator, &arr, {}, comptime std.sort.asc(i32)); 100 | try testing.expectEqualSlices(ItemsType, &expectedASC, &arr); 101 | } 102 | { 103 | var arr = items; 104 | try zort.timSort(ItemsType, testing.allocator, &arr, {}, comptime std.sort.desc(i32)); 105 | try testing.expectEqualSlices(ItemsType, &expectedDESC, &arr); 106 | } 107 | } 108 | 109 | test "tail" { 110 | { 111 | var arr = items; 112 | try zort.tailSort(ItemsType, testing.allocator, &arr, {}, comptime std.sort.asc(i32)); 113 | try testing.expectEqualSlices(ItemsType, &expectedASC, &arr); 114 | } 115 | { 116 | var arr = items; 117 | try zort.tailSort(ItemsType, testing.allocator, &arr, {}, comptime std.sort.desc(i32)); 118 | try testing.expectEqualSlices(ItemsType, &expectedDESC, &arr); 119 | } 120 | } 121 | 122 | test "twin" { 123 | { 124 | var arr = items; 125 | try zort.twinSort(ItemsType, testing.allocator, &arr, {}, comptime std.sort.asc(i32)); 126 | try testing.expectEqualSlices(ItemsType, &expectedASC, &arr); 127 | } 128 | { 129 | var arr = items; 130 | try zort.twinSort(ItemsType, testing.allocator, &arr, {}, comptime std.sort.desc(i32)); 131 | try testing.expectEqualSlices(ItemsType, &expectedDESC, &arr); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zort 2 | 3 | ![logo](/media/logo.png) 4 | 5 | Implementation of 13 sorting algorithms in Zig 6 | 7 | | Algorithm | Custom Comparison | Zero Allocation | 8 | | ----------------------- | ----------------- | --------------- | 9 | | Bubble | ✅ | ✅ | 10 | | Comb | ✅ | ✅ | 11 | | Heap | ✅ | ✅ | 12 | | Insertion | ✅ | ✅ | 13 | | Merge | ✅ | ❌ | 14 | | PDQ | ✅ | ✅ | 15 | | Quick | ✅ | ✅ | 16 | | Radix (no negative yet) | ❌ | ❌ | 17 | | Selection | ✅ | ✅ | 18 | | Shell | ✅ | ✅ | 19 | | Tail | ✅ | ❌ | 20 | | Tim | ✅ | ❌ | 21 | | Twin | ✅ | ❌ | 22 | 23 | ## Import 24 | 25 | 1. specific version: `zig fetch --save https://github.com/alichraghi/zort/archive/refs/tags/.tar.gz` 26 | 2. main branch version: `zig fetch --save git+https://github.com/alichraghi/zort.git` 27 | 28 | Example `build.zig` 29 | 30 | ```zig 31 | pub fn build(b: *std.Build) void { 32 | const exe = b.addExecutable(.{ ... }); 33 | 34 | const zort = b.dependency("zort", .{}); 35 | exe.root_module.addImport("zort", zort.module("zort")); 36 | } 37 | ``` 38 | 39 | ## Usage 40 | 41 | ```zig 42 | const zort = @import("zort"); 43 | 44 | fn asc(a: u8, b: u8) bool { 45 | return a < b; 46 | } 47 | 48 | pub fn main() !void { 49 | var arr = [_]u8{ 9, 1, 4, 12, 3, 4 }; 50 | try zort.quickSort(u8, &arr, asc); 51 | } 52 | ``` 53 | 54 | ## Benchmarks 55 | 56 | run this to see results on your machine: 57 | ``` 58 | zig build bench -Doptimize=ReleaseFast -- comb quick radix shell std_block std_heap std_pdq tail tim twin 59 | ``` 60 | 61 | ### 12th Gen Intel(R) Core(TM) i5-12400F 62 | 63 | ```mermaid 64 | gantt 65 | title Sorting (ascending) 10000000 usize 66 | dateFormat x 67 | axisFormat %S s 68 | section random 69 | quick 1.670: 0,1670 70 | comb 3.156: 0,3155 71 | shell 5.469: 0,5467 72 | radix 0.304: 0,304 73 | tim 2.893: 0,2893 74 | tail 2.400: 0,2399 75 | twin 2.377: 0,2377 76 | std_block 3.371: 0,3372 77 | std_pdq 2.250: 0,2250 78 | std_heap 5.512: 0,5514 79 | section sorted 80 | quick 0.829: 0,829 81 | comb 1.179: 0,1179 82 | shell 0.740: 0,740 83 | radix 0.301: 0,301 84 | tim 0.025: 0,25 85 | tail 0.058: 0,58 86 | twin 0.071: 0,71 87 | std_block 0.266: 0,266 88 | std_pdq 0.041: 0,41 89 | std_heap 3.912: 0,3913 90 | section reverse 91 | quick 1.544: 0,1544 92 | comb 1.375: 0,1375 93 | shell 1.378: 0,1378 94 | radix 0.303: 0,303 95 | tim 0.822: 0,822 96 | tail 1.364: 0,1364 97 | twin 1.280: 0,1280 98 | std_block 1.304: 0,1304 99 | std_pdq 0.199: 0,199 100 | std_heap 3.844: 0,3845 101 | section ascending saw 102 | quick 2.979: 0,2978 103 | comb 2.088: 0,2089 104 | shell 1.807: 0,1807 105 | radix 0.322: 0,322 106 | tim 0.473: 0,473 107 | tail 0.486: 0,486 108 | twin 0.465: 0,465 109 | std_block 1.129: 0,1129 110 | std_pdq 1.604: 0,1604 111 | std_heap 4.246: 0,4249 112 | section descending saw 113 | comb 1.949: 0,1949 114 | shell 1.694: 0,1694 115 | radix 0.299: 0,299 116 | tim 1.339: 0,1339 117 | tail 1.478: 0,1478 118 | twin 1.288: 0,1288 119 | std_block 1.618: 0,1618 120 | std_pdq 1.613: 0,1613 121 | std_heap 4.262: 0,4261 122 | ``` 123 | 124 | ```mermaid 125 | gantt 126 | title Sorting (ascending) 10000000 isize 127 | dateFormat x 128 | axisFormat %S s 129 | section random 130 | quick 1.691: 0,1691 131 | comb 3.084: 0,3084 132 | shell 5.410: 0,5413 133 | radix 0.332: 0,332 134 | tim 2.951: 0,2953 135 | tail 2.414: 0,2414 136 | twin 2.414: 0,2414 137 | std_block 3.346: 0,3346 138 | std_pdq 2.309: 0,2309 139 | std_heap 5.449: 0,5447 140 | section sorted 141 | quick 0.798: 0,798 142 | comb 1.166: 0,1166 143 | shell 0.739: 0,739 144 | radix 0.247: 0,247 145 | tim 0.025: 0,25 146 | tail 0.059: 0,59 147 | twin 0.048: 0,48 148 | std_block 0.270: 0,270 149 | std_pdq 0.039: 0,39 150 | std_heap 3.816: 0,3815 151 | section reverse 152 | quick 1.581: 0,1581 153 | comb 1.347: 0,1347 154 | shell 1.234: 0,1234 155 | radix 0.262: 0,262 156 | tim 0.044: 0,44 157 | tail 1.396: 0,1396 158 | twin 0.041: 0,41 159 | std_block 0.953: 0,953 160 | std_pdq 0.204: 0,204 161 | std_heap 3.713: 0,3713 162 | section ascending saw 163 | quick 5.516: 0,5516 164 | comb 2.064: 0,2064 165 | shell 1.632: 0,1632 166 | radix 0.280: 0,280 167 | tim 0.449: 0,449 168 | tail 0.466: 0,466 169 | twin 0.478: 0,478 170 | std_block 1.157: 0,1157 171 | std_pdq 1.618: 0,1618 172 | std_heap 4.281: 0,4279 173 | section descending saw 174 | comb 2.039: 0,2040 175 | shell 1.644: 0,1644 176 | radix 0.279: 0,279 177 | tim 0.465: 0,465 178 | tail 1.492: 0,1492 179 | twin 0.481: 0,481 180 | std_block 1.596: 0,1596 181 | std_pdq 1.576: 0,1576 182 | std_heap 4.199: 0,4199 183 | ``` 184 | 185 | ### Big Thank to 186 | 187 | [voroskoi](https://github.com/voroskoi) and other contributors 188 | -------------------------------------------------------------------------------- /bench/bench.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zort = @import("zort"); 3 | const gen = @import("generator.zig"); 4 | 5 | const INPUT_ITEMS = 10_000_000; 6 | const RUNS = 5; 7 | const TYPES = [_]type{ usize, isize }; 8 | const FLAVORS = .{ gen.random, gen.sorted, gen.reverse, gen.ascSaw, gen.descSaw }; 9 | 10 | const BenchResult = struct { 11 | command: []const u8, 12 | mean: u64, 13 | times: [RUNS]u64, 14 | tp: []const u8, 15 | flavor: []const u8, 16 | }; 17 | 18 | pub fn main() !void { 19 | // initialize the allocator 20 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 21 | const allocator = gpa.allocator(); 22 | defer _ = gpa.deinit(); 23 | 24 | // parse arguments 25 | const args = try std.process.argsAlloc(allocator); 26 | defer std.process.argsFree(allocator, args); 27 | 28 | if (args.len < 2) { 29 | std.debug.print("no algorithm(s) specified\nexiting...\n", .{}); 30 | return; 31 | } 32 | 33 | // prepare array for storing benchmark results 34 | var results = std.ArrayList(BenchResult).init(allocator); 35 | defer results.deinit(); 36 | 37 | inline for (TYPES) |tp| { 38 | inline for (FLAVORS) |flavor| { 39 | const flavor_name: []const u8 = switch (flavor) { 40 | gen.random => "random", 41 | gen.sorted => "sorted", 42 | gen.reverse => "reverse", 43 | gen.ascSaw => "ascending saw", 44 | gen.descSaw => "descending saw", 45 | else => unreachable, 46 | }; 47 | std.debug.print( 48 | "Generating {s} data ({d} items of {s})... ", 49 | .{ flavor_name, INPUT_ITEMS, @typeName(tp) }, 50 | ); 51 | const arr = try @call(.auto, flavor, .{ tp, allocator, INPUT_ITEMS }); 52 | defer allocator.free(arr); 53 | std.debug.print("Done. ", .{}); 54 | 55 | for (args[1..]) |arg| { 56 | std.debug.print("\nStarting {s} sort...", .{arg}); 57 | var res = try runIterations(tp, allocator, arg, arr); 58 | res.command = arg; 59 | res.tp = @typeName(tp); 60 | res.flavor = flavor_name; 61 | try results.append(res); 62 | } 63 | 64 | std.debug.print("\n", .{}); 65 | } 66 | } 67 | 68 | try writeMermaid(results); 69 | } 70 | 71 | fn checkSorted( 72 | comptime T: type, 73 | arr: []T, 74 | ) bool { 75 | var i: usize = 1; 76 | while (i < arr.len) : (i += 1) { 77 | if (arr[i - 1] > arr[i]) { 78 | return false; 79 | } 80 | } 81 | return true; 82 | } 83 | 84 | fn runIterations( 85 | comptime T: type, 86 | allocator: std.mem.Allocator, 87 | arg: [:0]u8, 88 | arr: []T, 89 | ) !BenchResult { 90 | var result: BenchResult = undefined; 91 | 92 | var i: usize = 0; 93 | var failed_runs: usize = 0; 94 | while (i < RUNS) : (i += 1) { 95 | const items = try allocator.dupe(T, arr); 96 | defer allocator.free(items); 97 | 98 | if (std.mem.eql(u8, arg, "bubble")) 99 | result.times[i] = try bench(zort.bubbleSort, .{ T, items, {}, comptime std.sort.asc(T) }) 100 | else if (std.mem.eql(u8, arg, "quick")) 101 | result.times[i] = try bench(zort.quickSort, .{ T, items, {}, comptime std.sort.asc(T) }) 102 | else if (std.mem.eql(u8, arg, "selection")) 103 | result.times[i] = try bench(zort.selectionSort, .{ T, items, {}, comptime std.sort.asc(T) }) 104 | else if (std.mem.eql(u8, arg, "comb")) 105 | result.times[i] = try bench(zort.combSort, .{ T, items, {}, comptime std.sort.asc(T) }) 106 | else if (std.mem.eql(u8, arg, "shell")) 107 | result.times[i] = try bench(zort.shellSort, .{ T, items, {}, comptime std.sort.asc(T) }) 108 | else if (std.mem.eql(u8, arg, "merge")) 109 | result.times[i] = try errbench( 110 | zort.mergeSort, 111 | .{ T, allocator, items, {}, comptime std.sort.asc(T) }, 112 | ) 113 | else if (std.mem.eql(u8, arg, "radix")) { 114 | result.times[i] = try errbench( 115 | zort.radixSort, 116 | .{ T, zort.SortOptions{}, allocator, items }, 117 | ); 118 | } else if (std.mem.eql(u8, arg, "tim")) 119 | result.times[i] = try errbench( 120 | zort.timSort, 121 | .{ T, allocator, items, {}, comptime std.sort.asc(T) }, 122 | ) 123 | else if (std.mem.eql(u8, arg, "tail")) 124 | result.times[i] = try errbench( 125 | zort.tailSort, 126 | .{ T, allocator, items, {}, comptime std.sort.asc(T) }, 127 | ) 128 | else if (std.mem.eql(u8, arg, "twin")) 129 | result.times[i] = try errbench( 130 | zort.twinSort, 131 | .{ T, allocator, items, {}, comptime std.sort.asc(T) }, 132 | ) 133 | else if (std.mem.eql(u8, arg, "std_block")) 134 | result.times[i] = try bench( 135 | std.sort.block, 136 | .{ T, items, {}, comptime std.sort.asc(T) }, 137 | ) 138 | else if (std.mem.eql(u8, arg, "std_pdq")) 139 | result.times[i] = try bench( 140 | std.sort.pdq, 141 | .{ T, items, {}, comptime std.sort.asc(T) }, 142 | ) 143 | else if (std.mem.eql(u8, arg, "std_heap")) 144 | result.times[i] = try bench( 145 | std.sort.heap, 146 | .{ T, items, {}, comptime std.sort.asc(T) }, 147 | ) 148 | else if (std.mem.eql(u8, arg, "std_insertion")) 149 | result.times[i] = try bench( 150 | std.sort.insertion, 151 | .{ T, items, {}, comptime std.sort.asc(T) }, 152 | ) 153 | else 154 | std.debug.panic("{s} is not a valid argument", .{arg}); 155 | 156 | if (!checkSorted(T, items)) { 157 | failed_runs += 1; 158 | } 159 | 160 | if (failed_runs == 0) { 161 | std.debug.print("\r{s:<20} round: {d:>2}/{d:<10} time: {d} ms", .{ arg, i + 1, RUNS, result.times[i] }); 162 | } else { 163 | std.debug.print("\r{s:<20} round: {d:>2}/{d:<10} time: {d} ms failed: {d} ", .{ arg, i + 1, RUNS, result.times[i], failed_runs }); 164 | } 165 | } 166 | 167 | var sum: usize = 0; 168 | for (result.times) |time| { 169 | sum += time; 170 | } 171 | result.mean = sum / RUNS; 172 | 173 | return result; 174 | } 175 | 176 | fn bench(func: anytype, args: anytype) anyerror!u64 { 177 | var timer = try std.time.Timer.start(); 178 | @call(.auto, func, args); 179 | return timer.read() / std.time.ns_per_ms; 180 | } 181 | 182 | fn errbench(func: anytype, args: anytype) anyerror!u64 { 183 | var timer = try std.time.Timer.start(); 184 | try @call(.auto, func, args); 185 | return timer.read() / std.time.ns_per_ms; 186 | } 187 | 188 | fn writeMermaid(results: std.ArrayList(BenchResult)) !void { 189 | const stdout = std.io.getStdOut().writer(); 190 | 191 | var curr_type: []const u8 = ""; 192 | var curr_flavor: []const u8 = ""; 193 | 194 | const header_top = 195 | \\ 196 | \\You can paste the following code snippet to a Markdown file: 197 | \\ 198 | ; 199 | const header_bottom = 200 | \\ dateFormat x 201 | \\ axisFormat %S s 202 | ; 203 | 204 | try stdout.print("\n{s}\n", .{header_top}); 205 | 206 | for (results.items) |res| { 207 | if (!std.mem.eql(u8, res.tp, curr_type)) { 208 | if (curr_type.len != 0) { 209 | _ = try stdout.write("```\n\n"); 210 | } 211 | try stdout.print( 212 | "```mermaid\ngantt\n title Sorting (ascending) {d} {s}\n{s}\n", 213 | .{ INPUT_ITEMS, res.tp, header_bottom }, 214 | ); 215 | curr_type = res.tp; 216 | curr_flavor = ""; 217 | } 218 | if (!std.mem.eql(u8, res.flavor, curr_flavor)) { 219 | try stdout.print(" section {s}\n", .{res.flavor}); 220 | curr_flavor = res.flavor; 221 | } 222 | try stdout.print(" {s} {d:.3}: 0,{d}\n", .{ 223 | res.command, 224 | @as(f16, @floatFromInt(res.mean)) / std.time.ms_per_s, 225 | res.mean, 226 | }); 227 | } 228 | 229 | _ = try stdout.write("```\n"); 230 | } 231 | -------------------------------------------------------------------------------- /src/radix.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const SortOrder = enum { 4 | ascending, 5 | descending, 6 | }; 7 | 8 | pub const SortOptions = struct { 9 | order: SortOrder = .ascending, 10 | _byte_order: SortOrder = .ascending, 11 | key_field: ?@Type(.enum_literal) = null, 12 | fn canonicalize(comptime self: SortOptions) SortOptions { 13 | return .{ 14 | .order = self.order, 15 | ._byte_order = self.order, 16 | .key_field = self.key_field, 17 | }; 18 | } 19 | fn reversed(comptime self: SortOptions) SortOptions { 20 | return .{ 21 | .order = self.order, 22 | ._byte_order = switch (self.order) { 23 | .ascending => .descending, 24 | .descending => .ascending, 25 | }, 26 | .key_field = self.key_field, 27 | }; 28 | } 29 | }; 30 | 31 | const INSSORT_CUTOFF = 55; 32 | 33 | inline fn lt(comptime T: type, comptime options: SortOptions, a: T, b: T) bool { 34 | if (options.key_field) |key_field| { 35 | const a_key = @field(a, @tagName(key_field)); 36 | const b_key = @field(b, @tagName(key_field)); 37 | return switch (options.order) { 38 | .ascending => a_key < b_key, 39 | .descending => b_key < a_key, 40 | }; 41 | } 42 | return switch (options.order) { 43 | .ascending => a < b, 44 | .descending => b < a, 45 | }; 46 | } 47 | 48 | inline fn insSort(comptime T: type, comptime options: SortOptions, noalias array: [*]T, len: usize) void { 49 | for (1..len) |i| { 50 | const tmp = array[i]; 51 | var j: usize = i; 52 | while (j > 0) : (j -= 1) { 53 | if (lt(T, options, array[j - 1], tmp)) { 54 | break; 55 | } 56 | array[j] = array[j - 1]; 57 | } 58 | array[j] = tmp; 59 | } 60 | } 61 | 62 | inline fn insSortIntoOtherArray(comptime T: type, comptime options: SortOptions, noalias src: [*]T, noalias dst: [*]T, len: usize) void { 63 | dst[0] = src[0]; 64 | for (1..len) |i| { 65 | const tmp = src[i]; 66 | var j: usize = i; 67 | while (j > 0) : (j -= 1) { 68 | if (lt(T, options, dst[j - 1], tmp)) { 69 | break; 70 | } 71 | dst[j] = dst[j - 1]; 72 | } 73 | dst[j] = tmp; 74 | } 75 | } 76 | 77 | inline fn truncate(comptime U: type, x: anytype) U { 78 | const T = @TypeOf(x); 79 | if (@bitSizeOf(T) >= @bitSizeOf(U)) { 80 | return @truncate(x); 81 | } 82 | return x; 83 | } 84 | 85 | inline fn readOneByte(comptime T: type, comptime options: SortOptions, comptime idx: usize, x: T) u8 { 86 | if (options.key_field) |key_field| { 87 | const key = @field(x, @tagName(key_field)); 88 | return readOneByte(FieldType(T, key_field), .{}, idx, key); 89 | } 90 | if (T == f32) { 91 | return readOneByte(u32, .{}, idx, @bitCast(x)); 92 | } 93 | if (T == f64) { 94 | return readOneByte(u64, .{}, idx, @bitCast(x)); 95 | } 96 | const U = comptime std.meta.Int(.unsigned, @bitSizeOf(T)); 97 | const shift = comptime (@sizeOf(T) - 1 - idx) * 8; 98 | if (idx == 0) { 99 | return truncate( 100 | u8, 101 | (@as(U, @bitCast(@as(T, std.math.minInt(T)))) ^ @as(U, @bitCast(x))) >> shift, 102 | ); 103 | } 104 | return truncate(u8, @as(U, @bitCast(x)) >> shift); 105 | } 106 | 107 | inline fn readTwoBytes(comptime T: type, comptime options: SortOptions, comptime idx: usize, x: T) u16 { 108 | if (options.key_field) |key_field| { 109 | const key = @field(x, @tagName(key_field)); 110 | return readTwoBytes(FieldType(T, key_field), .{}, idx, key); 111 | } 112 | if (T == f32) { 113 | return readTwoBytes(u32, .{}, idx, @bitCast(x)); 114 | } 115 | if (T == f64) { 116 | return readTwoBytes(u64, .{}, idx, @bitCast(x)); 117 | } 118 | const U = comptime std.meta.Int(.unsigned, @bitSizeOf(T)); 119 | const shift = comptime (@sizeOf(T) - 2 - idx) * 8; 120 | if (idx == 0) { 121 | return truncate( 122 | u16, 123 | (@as(U, @bitCast(@as(T, std.math.minInt(T)))) ^ @as(U, @bitCast(x))) >> shift, 124 | ); 125 | } 126 | return truncate(u16, @as(U, @bitCast(x)) >> shift); 127 | } 128 | 129 | fn FieldType(comptime T: type, comptime field: @Type(.enum_literal)) type { 130 | return std.meta.fieldInfo(T, field).type; 131 | } 132 | 133 | fn SortKeyType(comptime T: type, comptime options: SortOptions) type { 134 | if (options.key_field) |key_field| { 135 | return FieldType(T, key_field); 136 | } 137 | return T; 138 | } 139 | 140 | fn radixSortByBytesAdaptive( 141 | comptime T: type, 142 | comptime options: SortOptions, 143 | comptime Ubucket: type, 144 | comptime idx: usize, 145 | comptime array_is_final_destination: bool, 146 | comptime BYTES_PER_LEVEL: usize, 147 | noalias array: [*]T, 148 | noalias scratch: [*]T, 149 | arr_len: usize, 150 | noalias buckets: [*]Ubucket, 151 | ) void { 152 | comptime { 153 | std.debug.assert(BYTES_PER_LEVEL == 1 or BYTES_PER_LEVEL == 2); 154 | } 155 | 156 | var i: usize = 0; 157 | var lo: usize = 0; 158 | const buckets_len = comptime if (BYTES_PER_LEVEL == 2) 0x10000 else 0x100; 159 | const bucketsize = buckets; 160 | const bucketindex = buckets + buckets_len; 161 | const Key = SortKeyType(T, options); 162 | const is_first_byte_of_float_key = comptime if (Key == f32 or Key == f64) idx == 0 else false; 163 | 164 | const readBucket = comptime if (BYTES_PER_LEVEL == 2) readTwoBytes else readOneByte; 165 | 166 | i = 0; 167 | while (i < buckets_len) : (i += 1) { 168 | bucketsize[i] = 0; 169 | } 170 | 171 | i = 0; 172 | while (i < arr_len) : (i += 1) { 173 | const bucket = readBucket(T, options, idx, array[i]); 174 | bucketsize[bucket] += 1; 175 | } 176 | 177 | if (!is_first_byte_of_float_key) { 178 | switch (options._byte_order) { 179 | .ascending => { 180 | bucketindex[0] = 0; 181 | i = 1; 182 | while (i < buckets_len) : (i += 1) { 183 | bucketindex[i] = bucketindex[i - 1] + bucketsize[i - 1]; 184 | } 185 | }, 186 | .descending => { 187 | bucketindex[buckets_len - 1] = 0; 188 | i = buckets_len - 1; 189 | while (i > 0) : (i -= 1) { 190 | bucketindex[i - 1] = bucketindex[i] + bucketsize[i]; 191 | } 192 | }, 193 | } 194 | } else { 195 | switch (options._byte_order) { 196 | .ascending => { 197 | bucketindex[buckets_len - 1] = 0; 198 | i = buckets_len - 1; 199 | while (i > buckets_len / 2) : (i -= 1) { 200 | bucketindex[i - 1] = bucketindex[i] + bucketsize[i]; 201 | } 202 | bucketindex[0] = bucketindex[buckets_len / 2] + bucketsize[buckets_len / 2]; 203 | i = 1; 204 | while (i < buckets_len / 2) : (i += 1) { 205 | bucketindex[i] = bucketindex[i - 1] + bucketsize[i - 1]; 206 | } 207 | }, 208 | .descending => { 209 | bucketindex[buckets_len / 2 - 1] = 0; 210 | i = buckets_len / 2 - 1; 211 | while (i > 0) : (i -= 1) { 212 | bucketindex[i - 1] = bucketindex[i] + bucketsize[i]; 213 | } 214 | bucketindex[buckets_len / 2] = bucketindex[0] + bucketsize[0]; 215 | i = buckets_len / 2 + 1; 216 | while (i < buckets_len) : (i += 1) { 217 | bucketindex[i] = bucketindex[i - 1] + bucketsize[i - 1]; 218 | } 219 | }, 220 | } 221 | } 222 | 223 | i = 0; 224 | while (i < arr_len) : (i += 1) { 225 | const bucket = readBucket(T, options, idx, array[i]); 226 | scratch[bucketindex[bucket]] = array[i]; 227 | bucketindex[bucket] += 1; 228 | } 229 | 230 | const next_idx = comptime idx + BYTES_PER_LEVEL; 231 | if (next_idx >= @sizeOf(Key)) { 232 | if (array_is_final_destination) { 233 | i = 0; 234 | while (i < arr_len) : (i += 1) { 235 | array[i] = scratch[i]; 236 | } 237 | } 238 | return; 239 | } 240 | 241 | if (!is_first_byte_of_float_key) { 242 | switch (options._byte_order) { 243 | .ascending => { 244 | i = 0; 245 | while (i < buckets_len) : (i += 1) { 246 | const len = bucketsize[i]; 247 | const hi = lo + len; 248 | recurse(T, options, Ubucket, idx, array_is_final_destination, BYTES_PER_LEVEL, array, scratch, buckets, lo, len); 249 | lo = hi; 250 | } 251 | }, 252 | .descending => { 253 | i = buckets_len; 254 | while (i > 0) { 255 | i -= 1; 256 | const len = bucketsize[i]; 257 | const hi = lo + len; 258 | recurse(T, options, Ubucket, idx, array_is_final_destination, BYTES_PER_LEVEL, array, scratch, buckets, lo, len); 259 | lo = hi; 260 | } 261 | }, 262 | } 263 | } else { 264 | const reversed_options = comptime options.reversed(); 265 | switch (options._byte_order) { 266 | .ascending => { 267 | i = buckets_len; 268 | while (i > buckets_len / 2) { 269 | i -= 1; 270 | const len = bucketsize[i]; 271 | const hi = lo + len; 272 | recurse(T, reversed_options, Ubucket, idx, array_is_final_destination, BYTES_PER_LEVEL, array, scratch, buckets, lo, len); 273 | lo = hi; 274 | } 275 | i = 0; 276 | while (i < buckets_len / 2) : (i += 1) { 277 | const len = bucketsize[i]; 278 | const hi = lo + len; 279 | recurse(T, options, Ubucket, idx, array_is_final_destination, BYTES_PER_LEVEL, array, scratch, buckets, lo, len); 280 | lo = hi; 281 | } 282 | }, 283 | .descending => { 284 | i = buckets_len / 2; 285 | while (i > 0) { 286 | i -= 1; 287 | const len = bucketsize[i]; 288 | const hi = lo + len; 289 | recurse(T, options, Ubucket, idx, array_is_final_destination, BYTES_PER_LEVEL, array, scratch, buckets, lo, len); 290 | lo = hi; 291 | } 292 | i = buckets_len / 2; 293 | while (i < buckets_len) : (i += 1) { 294 | const len = bucketsize[i]; 295 | const hi = lo + len; 296 | recurse(T, reversed_options, Ubucket, idx, array_is_final_destination, BYTES_PER_LEVEL, array, scratch, buckets, lo, len); 297 | lo = hi; 298 | } 299 | }, 300 | } 301 | } 302 | } 303 | 304 | inline fn recurse( 305 | comptime T: type, 306 | comptime options: SortOptions, 307 | comptime Ubucket: type, 308 | comptime idx: usize, 309 | comptime array_is_final_destination: bool, 310 | comptime BYTES_PER_LEVEL: usize, 311 | noalias array: [*]T, 312 | noalias scratch: [*]T, 313 | noalias buckets: [*]Ubucket, 314 | lo: usize, 315 | len: usize, 316 | ) void { 317 | const next_idx = comptime idx + BYTES_PER_LEVEL; 318 | const buckets_len = comptime if (BYTES_PER_LEVEL == 2) 0x10000 else 0x100; 319 | const bucketindex = buckets + buckets_len; 320 | if (BYTES_PER_LEVEL == 2 and next_idx + 1 < @sizeOf(T) and len >= 0x10000) { 321 | radixSortByBytesAdaptive( 322 | T, 323 | options, 324 | Ubucket, 325 | next_idx, 326 | !array_is_final_destination, 327 | 2, 328 | scratch + lo, 329 | array + lo, 330 | len, 331 | bucketindex, 332 | ); 333 | } else if (len > INSSORT_CUTOFF) { 334 | radixSortByBytesAdaptive( 335 | T, 336 | options, 337 | Ubucket, 338 | next_idx, 339 | !array_is_final_destination, 340 | 1, 341 | scratch + lo, 342 | array + lo, 343 | len, 344 | bucketindex, 345 | ); 346 | } else if (len > 1) { 347 | if (array_is_final_destination) { 348 | insSortIntoOtherArray(T, options, scratch + lo, array + lo, len); 349 | } else { 350 | insSort(T, options, scratch + lo, len); 351 | } 352 | } else if (len == 1 and array_is_final_destination) { 353 | array[lo] = scratch[lo]; 354 | } 355 | } 356 | 357 | pub fn radixSort( 358 | comptime T: type, 359 | comptime _options: SortOptions, 360 | allocator: std.mem.Allocator, 361 | arr: []T, 362 | ) std.mem.Allocator.Error!void { 363 | const options = comptime _options.canonicalize(); 364 | const Key = SortKeyType(T, options); 365 | // the max number of buckets needed is for the case where we consume 366 | // 2 bytes at a time the whole way through. 367 | // In that case, we'll need 0x10000 * (n_levels+1) buckets. 368 | const max_buckets = (((@bitSizeOf(Key) + 15) / 16) + 1) * 0x10000; 369 | 370 | // using usize for buckets is pretty wasteful - most arrays have <2^32 elements. 371 | // this has a quite real performance cost because fewer buckets fit in cache. 372 | // so let's have a u32 version? 373 | const scratch = try allocator.alloc(T, arr.len); 374 | defer allocator.free(scratch); 375 | if (arr.len <= std.math.maxInt(u32) and @bitSizeOf(usize) > 32) { 376 | const buckets = try allocator.alloc(u32, max_buckets); 377 | defer allocator.free(buckets); 378 | if (@sizeOf(T) > 1 and arr.len >= 0x10000) { 379 | radixSortByBytesAdaptive( 380 | T, 381 | options, 382 | u32, 383 | 0, 384 | true, 385 | 2, 386 | arr.ptr, 387 | scratch.ptr, 388 | arr.len, 389 | buckets.ptr, 390 | ); 391 | } else { 392 | radixSortByBytesAdaptive( 393 | T, 394 | options, 395 | u32, 396 | 0, 397 | true, 398 | 1, 399 | arr.ptr, 400 | scratch.ptr, 401 | arr.len, 402 | buckets.ptr, 403 | ); 404 | } 405 | } else { 406 | const buckets = try allocator.alloc(usize, max_buckets); 407 | defer allocator.free(buckets); 408 | if (@sizeOf(T) > 1 and arr.len >= 0x10000) { 409 | radixSortByBytesAdaptive( 410 | T, 411 | options, 412 | usize, 413 | 0, 414 | true, 415 | 2, 416 | arr.ptr, 417 | scratch.ptr, 418 | arr.len, 419 | buckets.ptr, 420 | ); 421 | } else { 422 | radixSortByBytesAdaptive( 423 | T, 424 | options, 425 | usize, 426 | 0, 427 | true, 428 | 1, 429 | arr.ptr, 430 | scratch.ptr, 431 | arr.len, 432 | buckets.ptr, 433 | ); 434 | } 435 | } 436 | } 437 | -------------------------------------------------------------------------------- /src/tim.zig: -------------------------------------------------------------------------------- 1 | //! By VÖRÖSKŐI András 2 | //! This implementation is based on the go version found here: 3 | //! https://github.com/psilva261/timsort 4 | //! I have also read the java version found here: 5 | //! https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/util/TimSort.java 6 | 7 | const std = @import("std"); 8 | 9 | /// This is the minimum sized sequence that will be merged. Shorter 10 | /// sequences will be lengthened by calling `binarySort()`. If the entire 11 | /// array is less than this length, no merges will be performed. 12 | /// 13 | /// This variable is also used by `minRunLength()` function to determine 14 | /// minimal run length used in `binarySort()`. 15 | /// 16 | /// This constant should be a power of two! 17 | const MIN_MERGE = 32; 18 | 19 | /// Runs-to-be-merged stack size (which cannot be expanded). 20 | const STACK_LENGTH = 85; 21 | 22 | /// Maximum initial size of tmp array, which is used for merging. The array 23 | /// can grow to accommodate demand. 24 | 25 | // Unlike Tim's original C version, we do not allocate this much storage 26 | // when sorting smaller arrays. 27 | const TMP_SIZE = 256; 28 | 29 | /// When we get into galloping mode, we stay there until both runs win less 30 | /// often than MIN_GALLOP consecutive times. 31 | const MIN_GALLOP = 7; 32 | 33 | pub fn timSort( 34 | comptime T: type, 35 | allocator: std.mem.Allocator, 36 | items: []T, 37 | context: anytype, 38 | comptime cmp: fn (context: @TypeOf(context), lhs: T, rhs: T) bool, 39 | ) anyerror!void { 40 | // already sorted 41 | if (items.len < 2) return; 42 | 43 | // If the slice is small do a binarySort. 44 | if (items.len < MIN_MERGE) { 45 | const init_run_len = countRun(T, items, context, cmp); 46 | 47 | binarySort(T, items, init_run_len, context, cmp); 48 | return; 49 | } 50 | 51 | // Real TimSort starts here 52 | var ts = try TimSort(T, context, cmp).init(allocator, items); 53 | defer ts.deinit(); 54 | 55 | const min_run = ts.minRunLength(); 56 | var lo: usize = 0; 57 | var remain: usize = items.len; 58 | 59 | while (true) { 60 | var run_len: usize = countRun(T, items[lo..], context, cmp); 61 | 62 | // If run is short extend to min(min_run, remain). 63 | if (run_len < min_run) { 64 | const force = if (remain <= min_run) remain else min_run; 65 | 66 | binarySort(T, items[lo .. lo + force], run_len, context, cmp); 67 | run_len = force; 68 | } 69 | 70 | // Push run into pending-run stack, any maybe merge 71 | ts.pushRun(lo, run_len); 72 | try ts.mergeCollapse(); 73 | 74 | // Advance to find next run 75 | lo += run_len; 76 | remain -= run_len; 77 | if (remain == 0) break; 78 | } 79 | 80 | try ts.mergeForceCollapse(); 81 | } 82 | 83 | fn countRun( 84 | comptime T: type, 85 | items: []T, 86 | context: anytype, 87 | comptime cmp: fn (context: @TypeOf(context), lhs: T, rhs: T) bool, 88 | ) usize { 89 | var run_hi: usize = 1; 90 | 91 | if (run_hi == items.len) return 1; 92 | 93 | // Find end of run and reverse range if descending. 94 | if (cmp(context, items[run_hi], items[0])) { 95 | // descending 96 | run_hi += 1; 97 | 98 | while (run_hi < items.len and cmp(context, items[run_hi], items[run_hi - 1])) run_hi += 1; 99 | 100 | std.mem.reverse(T, items[0..run_hi]); 101 | } else { 102 | // ascending 103 | while (run_hi < items.len and !cmp(context, items[run_hi], items[run_hi - 1])) run_hi += 1; 104 | } 105 | 106 | return run_hi; 107 | } 108 | 109 | fn binarySort( 110 | comptime T: type, 111 | items: []T, 112 | orig_start: usize, 113 | context: anytype, 114 | comptime cmp: fn (context: @TypeOf(context), lhs: T, rhs: T) bool, 115 | ) void { 116 | var start: usize = orig_start; 117 | if (start == 0) start += 1; 118 | 119 | while (start < items.len) : (start += 1) { 120 | const pivot = items[start]; 121 | 122 | // Set left and right to the index where items[start] (pivot) belongs. 123 | var left: usize = 0; 124 | var right: usize = start; 125 | 126 | // Invariants: 127 | // pivot >= all in [0, left) 128 | // pivot < all in [right, start) 129 | while (left < right) { 130 | // https://ai.googleblog.com/2006/06/extra-extra-read-all-about-it-nearly.html 131 | const mid = (left +| right) >> 1; 132 | 133 | if (cmp(context, pivot, items[mid])) right = mid else left = mid + 1; 134 | } 135 | 136 | // The invariants still hold: pivot >= all in [lo, left) and 137 | // pivot < all in [left, start), so pivot belongs at left. Note 138 | // that if there are elements equal to pivot, left points to the 139 | // first slot after them -- that's why this sort is stable. 140 | // Slide elements over to make room to make room for pivot. 141 | 142 | const n = start - left; // the number of elements to move 143 | 144 | std.debug.assert(left + 1 > left); 145 | std.mem.copyBackwards(T, items[left + 1 ..], items[left .. left + n]); 146 | 147 | items[left] = pivot; 148 | } 149 | } 150 | 151 | fn TimSort( 152 | comptime T: type, 153 | comptime context: anytype, 154 | comptime cmp: fn (context: @TypeOf(context), lhs: T, rhs: T) bool, 155 | ) type { 156 | return struct { 157 | allocator: std.mem.Allocator, 158 | items: []T, 159 | min_gallop: usize = MIN_GALLOP, 160 | 161 | // Timsort temporary stacks 162 | tmp: []T, 163 | run_base: []usize, 164 | run_len: []usize, 165 | pending: usize = 0, 166 | 167 | // Comparing information 168 | context: @TypeOf(context) = context, 169 | cmp: *const fn (@TypeOf(context), T, T) bool = cmp, 170 | 171 | fn init(allocator: std.mem.Allocator, items: []T) !@This() { 172 | // Adjust tmp_size 173 | var tmp_size: usize = TMP_SIZE; 174 | if (items.len < 2 * tmp_size) tmp_size = items.len / 2; 175 | return @This(){ 176 | .tmp = try allocator.alloc(T, tmp_size), 177 | .run_base = try allocator.alloc(usize, STACK_LENGTH), 178 | .run_len = try allocator.alloc(usize, STACK_LENGTH), 179 | .allocator = allocator, 180 | .items = items, 181 | }; 182 | } 183 | 184 | fn deinit(self: @This()) void { 185 | self.allocator.free(self.tmp); 186 | self.allocator.free(self.run_base); 187 | self.allocator.free(self.run_len); 188 | } 189 | 190 | /// Returns the minimum acceptable run length for an array of the specified 191 | /// length. Natural runs shorter than this will be extended with binarySort. 192 | /// 193 | /// Roughly speaking, the computation is: 194 | /// If n < MIN_MERGE, return n (it's too small to bother with fancy stuff). 195 | /// Else if n is an exact power of 2, return MIN_MERGE/2. 196 | /// Else return an int k, MIN_MERGE/2 <= k <= MIN_MERGE, such that n/k 197 | /// is close to, but strictly less than, an exact power of 2. 198 | fn minRunLength(self: @This()) usize { 199 | var n: usize = self.items.len; 200 | var r: usize = 0; 201 | while (n >= MIN_MERGE) { 202 | r |= (n & 1); 203 | n >>= 1; 204 | } 205 | 206 | return n + r; 207 | } 208 | 209 | /// Pushes the specified run onto the pending-run stack. 210 | fn pushRun(self: *@This(), run_base: usize, run_len: usize) void { 211 | self.run_base[self.pending] = run_base; 212 | self.run_len[self.pending] = run_len; 213 | self.pending += 1; 214 | } 215 | 216 | /// Examines the stack of runs waiting to be merged and merges adjacent runs 217 | /// until the stack invariants are reestablished: 218 | /// 219 | /// 1. runLen[i - 3] > runLen[i - 2] + runLen[i - 1] 220 | /// 2. runLen[i - 2] > runLen[i - 1] 221 | /// 222 | /// This method is called each time a new run is pushed onto the stack, 223 | /// so the invariants are guaranteed to hold for i < stackSize upon 224 | /// entry to the method. 225 | fn mergeCollapse(self: *@This()) !void { 226 | while (self.pending > 1) { 227 | var n: usize = self.pending - 2; 228 | if ((n > 0 and self.run_len[n - 1] <= self.run_len[n] + self.run_len[n + 1]) or 229 | (n > 1 and self.run_len[n - 2] <= self.run_len[n - 1] + self.run_len[n])) 230 | { 231 | if (self.run_len[n - 1] < self.run_len[n + 1]) n -= 1; 232 | try self.mergeAt(n); 233 | } else if (self.run_len[n] <= self.run_len[n + 1]) { 234 | try self.mergeAt(n); 235 | } else break; // invariant is established 236 | } 237 | } 238 | 239 | /// Merges all runs on the stack until only one remains. This method is 240 | /// called once, to complete the sort. 241 | fn mergeForceCollapse(self: *@This()) !void { 242 | while (self.pending > 1) { 243 | var n: usize = self.pending - 2; 244 | if (n > 0 and self.run_len[n - 1] < self.run_len[n + 1]) n -= 1; 245 | try self.mergeAt(n); 246 | } 247 | } 248 | 249 | /// Merges the two runs at stack indices `i` and `i+1`. Run i must be 250 | /// the penultimate or antepenultimate run on the pending stack. In other words, 251 | /// `i` must be equal to self.pending-2 or self.pending-3. 252 | fn mergeAt(self: *@This(), i: usize) !void { 253 | var base1: usize = self.run_base[i]; 254 | var len1: usize = self.run_len[i]; 255 | const base2: usize = self.run_base[i + 1]; 256 | var len2: usize = self.run_len[i + 1]; 257 | 258 | // Record the length of the combined runs; if `i` is the 3rd-last 259 | // run now, also slide over the last run (which isn't involved 260 | // in this merge). The current run (`i+1`) goes away in any case. 261 | self.run_len[i] = len1 + len2; 262 | if (i == @as(isize, @intCast(self.pending)) - 3) { 263 | self.run_base[i + 1] = self.run_base[i + 2]; 264 | self.run_len[i + 1] = self.run_len[i + 2]; 265 | } 266 | self.pending -= 1; 267 | 268 | // Find where the first element of run2 goes in run1. Prior elements 269 | // in run1 can be ignored (because they're already in place). 270 | const k = self.gallopRight(self.items[base2], self.items, base1, len1, 0); 271 | base1 += k; 272 | len1 -= k; 273 | if (len1 == 0) return; 274 | 275 | // Find where the last element of run1 goes in run2. Subsequent elements 276 | // in run2 can be ignored (because they're already in place). 277 | len2 = self.gallopLeft(self.items[base1 + len1 - 1], self.items, base2, len2, len2 - 1); 278 | if (len2 == 0) return; 279 | 280 | // Merge remaining runs, using tmp array with min(len1, len2) elements 281 | if (len1 <= len2) 282 | try self.mergeLo(base1, len1, base2, len2) 283 | else 284 | try self.mergeHi(base1, len1, base2, len2); 285 | } 286 | 287 | /// Merges two adjacent runs in place, in a stable fashion. The first 288 | /// element of the first run must be greater than the first element of the 289 | /// second run (items[base1] > items[base2]), and the last element of the first run 290 | /// (items[base1 + len1-1]) must be greater than all elements of the second run. 291 | /// 292 | /// For performance, this method should be called only when len1 <= len2; 293 | /// its twin, mergeHi should be called if len1 >= len2. (Either method 294 | /// may be called if len1 == len2.) 295 | fn mergeLo( 296 | self: *@This(), 297 | base1: usize, 298 | orig_len1: usize, 299 | base2: usize, 300 | orig_len2: usize, 301 | ) !void { 302 | var len1: usize = orig_len1; 303 | var len2: usize = orig_len2; 304 | 305 | const tmp = try self.ensureCapacity(len1); 306 | 307 | std.mem.copyForwards(T, tmp[0..len1], self.items[base1 .. base1 + len1]); 308 | 309 | var cursor1: usize = 0; 310 | var cursor2: usize = base2; 311 | var dest: usize = base1; 312 | 313 | // Move first element of second run and deal with degenerate cases. 314 | self.items[dest] = self.items[cursor2]; 315 | dest += 1; 316 | cursor2 += 1; 317 | len2 -= 1; 318 | 319 | if (len2 == 0) { 320 | std.mem.copyForwards(T, self.items[dest .. dest + len1], tmp[0..len1]); 321 | return; 322 | } 323 | 324 | if (len1 == 1) { 325 | std.debug.assert(dest <= cursor2); 326 | std.mem.copyForwards(T, self.items[dest .. dest + len2], self.items[cursor2 .. cursor2 + len2]); 327 | self.items[dest + len2] = tmp[cursor1]; 328 | return; 329 | } 330 | 331 | var min_gallop: usize = self.min_gallop; 332 | 333 | outer: while (true) { 334 | var count1: usize = 0; 335 | var count2: usize = 0; 336 | 337 | // Do the straightforward thing until (if ever) one run starts 338 | // winning consistently. 339 | while (true) { 340 | if (self.cmp(self.context, self.items[cursor2], tmp[cursor1])) { 341 | self.items[dest] = self.items[cursor2]; 342 | dest += 1; 343 | cursor2 += 1; 344 | count2 += 1; 345 | count1 = 0; 346 | len2 -= 1; 347 | if (len2 == 0) break :outer; 348 | } else { 349 | self.items[dest] = tmp[cursor1]; 350 | dest += 1; 351 | cursor1 += 1; 352 | count1 += 1; 353 | count2 = 0; 354 | len1 -= 1; 355 | if (len1 == 1) break :outer; 356 | } 357 | 358 | if ((count1 | count2) >= min_gallop) break; 359 | } 360 | 361 | // One run is winning so consistently that galloping may be a 362 | // huge win. So try that, and continue galloping until (if ever) 363 | // neither run appears to be winning consistently anymore. 364 | while (true) { 365 | // gallopRight 366 | count1 = self.gallopRight(self.items[cursor2], tmp, cursor1, len1, 0); 367 | 368 | if (count1 != 0) { 369 | std.mem.copyForwards( 370 | T, 371 | self.items[dest .. dest + count1], 372 | tmp[cursor1 .. cursor1 + count1], 373 | ); 374 | dest += count1; 375 | cursor1 += count1; 376 | len1 -= count1; 377 | if (len1 <= 1) break :outer; 378 | } 379 | 380 | self.items[dest] = self.items[cursor2]; 381 | dest += 1; 382 | cursor2 += 1; 383 | len2 -= 1; 384 | if (len2 == 0) break :outer; 385 | 386 | // gallopLeft 387 | count2 = self.gallopLeft(tmp[cursor1], self.items, cursor2, len2, 0); 388 | 389 | if (count2 != 0) { 390 | std.debug.assert(dest <= cursor2); 391 | std.mem.copyForwards( 392 | T, 393 | self.items[dest .. dest + count2], 394 | self.items[cursor2 .. cursor2 + count2], 395 | ); 396 | dest += count2; 397 | cursor2 += count2; 398 | len2 -= count2; 399 | if (len2 == 0) break :outer; 400 | } 401 | 402 | self.items[dest] = tmp[cursor1]; 403 | dest += 1; 404 | cursor1 += 1; 405 | len1 -= 1; 406 | if (len1 == 1) break :outer; 407 | 408 | min_gallop -|= 1; 409 | 410 | if (count1 < min_gallop and count2 < min_gallop) break; 411 | } 412 | 413 | min_gallop += 2; // penalize for leaving gallop mode 414 | 415 | } // end of `outer` loop 416 | 417 | if (min_gallop < 1) min_gallop = 1; 418 | 419 | self.min_gallop = min_gallop; 420 | 421 | if (len1 == 1) { 422 | std.debug.assert(dest <= cursor2); 423 | std.mem.copyForwards(T, self.items[dest .. dest + len2], self.items[cursor2 .. cursor2 + len2]); 424 | self.items[dest + len2] = tmp[cursor1]; 425 | } else { 426 | std.mem.copyForwards(T, self.items[dest .. dest + len1], tmp[cursor1 .. cursor1 + len1]); 427 | } 428 | } 429 | 430 | /// Like mergeLo, except that this method should be called only if 431 | /// len1 >= len2; mergeLo should be called if len1 <= len2. (Either method 432 | /// may be called if len1 == len2.) 433 | fn mergeHi( 434 | self: *@This(), 435 | base1: usize, 436 | orig_len1: usize, 437 | base2: usize, 438 | orig_len2: usize, 439 | ) !void { 440 | var len1: usize = orig_len1; 441 | var len2: usize = orig_len2; 442 | 443 | const tmp = try self.ensureCapacity(len2); 444 | 445 | std.mem.copyForwards(T, tmp[0..len2], self.items[base2 .. base2 + len2]); 446 | 447 | var cursor1: usize = base1 + len1; 448 | var cursor2: usize = len2 - 1; 449 | var dest: usize = base2 + len2 - 1; 450 | 451 | // Move first element of second run and deal with degenerate cases. 452 | self.items[dest] = self.items[cursor1 - 1]; 453 | dest -= 1; 454 | cursor1 -= 1; 455 | len1 -= 1; 456 | 457 | if (len1 == 0) { 458 | dest -= len2 - 1; 459 | std.mem.copyForwards(T, self.items[dest .. dest + len2], tmp[0..len2]); 460 | return; 461 | } 462 | 463 | if (len2 == 1) { 464 | dest -= len1 - 1; 465 | cursor1 -= len1 - 1; 466 | std.debug.assert(dest > cursor1 - 1); 467 | std.mem.copyBackwards( 468 | T, 469 | self.items[dest .. dest + len1], 470 | self.items[cursor1 - 1 .. cursor1 - 1 + len1], 471 | ); 472 | self.items[dest - 1] = tmp[cursor2]; 473 | return; 474 | } 475 | 476 | var min_gallop: usize = self.min_gallop; 477 | 478 | outer: while (true) { 479 | var count1: usize = 0; 480 | var count2: usize = 0; 481 | 482 | // Do the straightforward thing until (if ever) one run starts 483 | // winning consistently. 484 | while (true) { 485 | if (self.cmp(self.context, tmp[cursor2], self.items[cursor1 - 1])) { 486 | self.items[dest] = self.items[cursor1 - 1]; 487 | dest -= 1; 488 | cursor1 -= 1; 489 | count1 += 1; 490 | count2 = 0; 491 | len1 -= 1; 492 | if (len1 == 0) break :outer; 493 | } else { 494 | self.items[dest] = tmp[cursor2]; 495 | dest -= 1; 496 | cursor2 -= 1; 497 | count2 += 1; 498 | count1 = 0; 499 | len2 -= 1; 500 | if (len2 == 1) break :outer; 501 | } 502 | 503 | if ((count1 | count2) >= min_gallop) break; 504 | } 505 | 506 | // One run is winning so consistently that galloping may be a 507 | // huge win. So try that, and continue galloping until (if ever) 508 | // neither run appears to be winning consistently anymore. 509 | while (true) { 510 | // gallopRight 511 | const gr = self.gallopRight(tmp[cursor2], self.items, base1, len1, len1 - 1); 512 | count1 = len1 - gr; 513 | 514 | if (count1 != 0) { 515 | dest -= count1; 516 | cursor1 -= count1; 517 | len1 -= count1; 518 | std.debug.assert(dest + 1 > cursor1); 519 | std.mem.copyBackwards(T, self.items[dest + 1 .. dest + 1 + count1], self.items[cursor1 .. cursor1 + count1]); 520 | if (len1 == 0) break :outer; 521 | } 522 | 523 | self.items[dest] = tmp[cursor2]; 524 | dest -= 1; 525 | cursor2 -= 1; 526 | len2 -= 1; 527 | if (len2 == 1) break :outer; 528 | 529 | // gallopLeft 530 | const gl = self.gallopLeft(self.items[cursor1 - 1], tmp, 0, len2, len2 - 1); 531 | count2 = len2 - gl; 532 | 533 | if (count2 != 0) { 534 | dest -= count2; 535 | cursor2 -= count2; 536 | len2 -= count2; 537 | std.mem.copyForwards( 538 | T, 539 | self.items[dest + 1 .. dest + 1 + count2], 540 | tmp[cursor2 + 1 .. cursor2 + 1 + count2], 541 | ); 542 | if (len2 <= 1) break :outer; 543 | } 544 | 545 | self.items[dest] = self.items[cursor1 - 1]; 546 | dest -= 1; 547 | cursor1 -= 1; 548 | len1 -= 1; 549 | if (len1 == 0) break :outer; 550 | 551 | min_gallop -|= 1; 552 | 553 | if (count1 < min_gallop and count2 < min_gallop) break; 554 | } 555 | 556 | min_gallop += 2; // penalize for leaving gallop mode 557 | 558 | } // end of `outer` loop 559 | 560 | if (min_gallop < 1) min_gallop = 1; 561 | 562 | self.min_gallop = min_gallop; 563 | 564 | if (len2 == 1) { 565 | dest -= len1; 566 | cursor1 -= len1; 567 | 568 | std.debug.assert(dest + 1 > cursor1); 569 | std.mem.copyBackwards(T, self.items[dest + 1 .. dest + 1 + len1], self.items[cursor1 .. cursor1 + len1]); 570 | self.items[dest] = tmp[cursor2]; 571 | } else { 572 | std.mem.copyForwards(T, self.items[dest + 1 - len2 .. dest + 1], tmp[0..len2]); 573 | } 574 | } 575 | 576 | /// Ensures that the external array tmp has at least the specified 577 | /// number of elements, increasing its size if necessary. The size 578 | /// increases exponentially to ensure amortized linear time complexity. 579 | fn ensureCapacity(self: *@This(), min_cap: usize) ![]T { 580 | if (self.tmp.len < min_cap) { 581 | const new_size = try std.math.ceilPowerOfTwo(usize, min_cap); 582 | self.tmp = try self.allocator.realloc(self.tmp, new_size); 583 | } 584 | 585 | return self.tmp; 586 | } 587 | 588 | /// Locates the position at which to insert the specified key into the 589 | /// specified sorted range; if the range contains an element equal to key, 590 | /// returns the index of the leftmost equal element. 591 | fn gallopLeft( 592 | self: @This(), 593 | key: T, 594 | items: []T, 595 | base: usize, 596 | len: usize, 597 | hint: usize, 598 | ) usize { 599 | var last_offset: usize = 0; 600 | var offset: usize = 1; 601 | 602 | if (cmp(context, items[base + hint], key)) { 603 | // Gallop right until items[base+hint+last_offset] < key <= items[base+hint+offset] 604 | const max_offset = len - hint; 605 | 606 | while (offset < max_offset and self.cmp(context, items[base + hint + offset], key)) { 607 | last_offset = offset; 608 | const ov = @shlWithOverflow(offset, @as(u6, 1)); 609 | if (ov[1] == 1) { 610 | break; 611 | } 612 | offset = ov[0] + 1; 613 | } 614 | 615 | if (offset > max_offset) offset = max_offset; 616 | 617 | // Make offsets relative to base 618 | last_offset += hint + 1; 619 | offset += hint; 620 | } else { // key <= items[base + hint] 621 | // Gallop left until items[base+hint-offset] < key <= items[base+hint-last_offset] 622 | const max_offset = hint + 1; 623 | while (offset < max_offset and !self.cmp(context, items[base + hint - offset], key)) { 624 | last_offset = offset; 625 | const ov = @shlWithOverflow(offset, @as(u6, 1)); 626 | if (ov[1] == 1) { 627 | break; 628 | } 629 | offset = ov[0] + 1; 630 | } 631 | 632 | if (offset > max_offset) offset = max_offset; 633 | 634 | // Make offsets relative to base 635 | const tmp = last_offset; 636 | last_offset = hint + 1 - offset; 637 | offset = hint - tmp; 638 | } 639 | 640 | // Now items[base+last_offset] < key <= items[base+offset], so key belongs somewhere 641 | // to the right of lastoffset but no farther right than offset. Do a binary 642 | // search, with invariant items[base + last_offset - 1] < key <= items[base + offset]. 643 | 644 | while (last_offset < offset) { 645 | const m = last_offset + (offset - last_offset) / 2; 646 | 647 | if (self.cmp(context, items[base + m], key)) 648 | last_offset = m + 1 649 | else 650 | offset = m; 651 | } 652 | 653 | return offset; 654 | } 655 | 656 | /// Like gallopLeft, except that if the range contains an element equal to 657 | /// key, gallopRight returns the index after the rightmost equal element. 658 | fn gallopRight( 659 | self: @This(), 660 | key: T, 661 | items: []T, 662 | base: usize, 663 | len: usize, 664 | hint: usize, 665 | ) usize { 666 | var last_offset: usize = 0; 667 | var offset: usize = 1; 668 | 669 | if (self.cmp(context, key, items[base + hint])) { 670 | // Gallop left until items[base+hint-offset] <= key < items[base+hint-last_offset] 671 | const max_offset = hint + 1; 672 | 673 | while (offset < max_offset and self.cmp(context, key, items[base + hint - offset])) { 674 | last_offset = offset; 675 | const ov = @shlWithOverflow(offset, @as(u6, 1)); 676 | if (ov[1] == 1) { 677 | break; 678 | } 679 | offset = ov[0] + 1; 680 | } 681 | 682 | if (offset > max_offset) offset = max_offset; 683 | 684 | // Make offsets relative to base 685 | const tmp = last_offset; 686 | last_offset = hint + 1 - offset; 687 | offset = hint - tmp; 688 | } else { // items[base + hint] <= key 689 | // Gallop right until items[base+hint+last_offset] <= key < items[base+hint+offset] 690 | const max_offset = len - hint; 691 | while (offset < max_offset and !self.cmp(context, key, items[base + hint + offset])) { 692 | last_offset = offset; 693 | const ov = @shlWithOverflow(offset, @as(u6, 1)); 694 | if (ov[1] == 1) { 695 | break; 696 | } 697 | offset = ov[0] + 1; 698 | } 699 | 700 | if (offset > max_offset) offset = max_offset; 701 | 702 | // Make offsets relative to base 703 | last_offset += hint + 1; 704 | offset += hint; 705 | } 706 | 707 | // Now items[base+last_offset] <= key < items[base+offset], so key belongs somewhere 708 | // to the right of lastoffset but no farther right than offset. Do a binary 709 | // search, with invariant items[base+last_offset-1] <= key < items[base+offset]. 710 | 711 | while (last_offset < offset) { 712 | const m = last_offset + (offset - last_offset) / 2; 713 | 714 | if (cmp(context, key, items[base + m])) 715 | offset = m 716 | else 717 | last_offset = m + 1; 718 | } 719 | 720 | return offset; 721 | } 722 | }; 723 | } 724 | 725 | test "timSort" { 726 | { 727 | var array = [_]usize{ 6, 4, 1, 4, 3, 2, 6, 7, 8, 9 }; 728 | const exp = [_]usize{ 1, 2, 3, 4, 4, 6, 6, 7, 8, 9 }; 729 | 730 | try timSort(usize, std.testing.allocator, &array, {}, comptime std.sort.asc(usize)); 731 | 732 | try std.testing.expectEqualSlices(usize, &exp, &array); 733 | } 734 | 735 | { 736 | var array = [_]usize{ 6, 4, 1, 4, 3, 2, 6, 7, 8, 9, 0 }; 737 | const exp = [_]usize{ 0, 1, 2, 3, 4, 4, 6, 6, 7, 8, 9 }; 738 | 739 | try timSort(usize, std.testing.allocator, &array, {}, comptime std.sort.asc(usize)); 740 | 741 | try std.testing.expectEqualSlices(usize, &exp, &array); 742 | } 743 | 744 | { 745 | var array = [_]usize{ 1, 4, 6, 4, 3, 2, 6, 7, 8, 9 }; 746 | const exp = [_]usize{ 9, 8, 7, 6, 6, 4, 4, 3, 2, 1 }; 747 | 748 | try timSort(usize, std.testing.allocator, &array, {}, comptime std.sort.desc(usize)); 749 | 750 | try std.testing.expectEqualSlices(usize, &exp, &array); 751 | } 752 | 753 | { 754 | var array = [_]usize{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 11, 22, 33, 44, 55, 66, 77, 88, 99, 1000, 0, 500 }; 755 | const exp = [_]usize{ 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 10, 10, 11, 20, 20, 22, 30, 30, 33, 40, 40, 44, 50, 50, 55, 60, 60, 66, 70, 70, 77, 80, 80, 88, 90, 90, 99, 100, 100, 500, 1000 }; 756 | 757 | try timSort(usize, std.testing.allocator, &array, {}, comptime std.sort.asc(usize)); 758 | 759 | try std.testing.expectEqualSlices(usize, &exp, &array); 760 | } 761 | 762 | { 763 | // ascending 764 | const TEST_TYPE = isize; 765 | const TESTS = 10; 766 | const ITEMS = 10_000; 767 | 768 | var rnd = std.Random.DefaultPrng.init(@intCast(std.time.milliTimestamp())); 769 | 770 | var tc: usize = 0; 771 | while (tc < TESTS) : (tc += 1) { 772 | var array = try std.ArrayList(TEST_TYPE).initCapacity(std.testing.allocator, ITEMS); 773 | defer array.deinit(std.testing.allocator); 774 | 775 | var item: usize = 0; 776 | while (item < ITEMS) : (item += 1) { 777 | const value = rnd.random().int(TEST_TYPE); 778 | array.appendAssumeCapacity(value); 779 | } 780 | var reference = try array.clone(std.testing.allocator); 781 | defer reference.deinit(std.testing.allocator); 782 | 783 | std.mem.sort(TEST_TYPE, reference.items, {}, comptime std.sort.asc(TEST_TYPE)); 784 | 785 | try timSort(TEST_TYPE, std.testing.allocator, array.items, {}, comptime std.sort.asc(TEST_TYPE)); 786 | 787 | try std.testing.expectEqualSlices(TEST_TYPE, reference.items, array.items); 788 | } 789 | } 790 | 791 | { 792 | // descending 793 | const TEST_TYPE = isize; 794 | const TESTS = 10; 795 | const ITEMS = 10_000; 796 | 797 | var rnd = std.Random.DefaultPrng.init(@intCast(std.time.milliTimestamp())); 798 | 799 | var tc: usize = 0; 800 | while (tc < TESTS) : (tc += 1) { 801 | var array = try std.ArrayList(TEST_TYPE).initCapacity(std.testing.allocator, ITEMS); 802 | defer array.deinit(std.testing.allocator); 803 | 804 | var item: usize = 0; 805 | while (item < ITEMS) : (item += 1) { 806 | const value = rnd.random().int(TEST_TYPE); 807 | array.appendAssumeCapacity(value); 808 | } 809 | var reference = try array.clone(std.testing.allocator); 810 | defer reference.deinit(std.testing.allocator); 811 | 812 | std.mem.sort(TEST_TYPE, reference.items, {}, comptime std.sort.desc(TEST_TYPE)); 813 | 814 | try timSort(TEST_TYPE, std.testing.allocator, array.items, {}, comptime std.sort.desc(TEST_TYPE)); 815 | 816 | try std.testing.expectEqualSlices(TEST_TYPE, reference.items, array.items); 817 | } 818 | } 819 | } 820 | --------------------------------------------------------------------------------