From ef29861d67204f21407e5e336408819ff693163d Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 18 May 2025 12:45:30 -0700 Subject: [PATCH] std: introduce orderedRemoveMany This algorithm is non-trivial and makes sense for any data structure that acts as an array list, so I thought it would make sense as a method. I have a real world case for this in a music player application (deleting queue items). Adds the method to: * ArrayList * ArrayHashMap * MultiArrayList --- lib/std/array_hash_map.zig | 53 ++++++++++++++++++++++++++++++ lib/std/array_list.zig | 56 +++++++++++++++++++++++++++++++- lib/std/multi_array_list.zig | 62 ++++++++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+), 1 deletion(-) diff --git a/lib/std/array_hash_map.zig b/lib/std/array_hash_map.zig index b0b9e19169c5..5abf558d0b2c 100644 --- a/lib/std/array_hash_map.zig +++ b/lib/std/array_hash_map.zig @@ -1282,6 +1282,33 @@ pub fn ArrayHashMapUnmanaged( self.removeByIndex(index, if (store_hash) {} else ctx, .ordered); } + /// Remove the entries indexed by `sorted_indexes`. The indexes to be + /// removed correspond to state before deletion. + /// + /// This operation is O(N). + /// + /// Asserts that each index to be removed is in bounds. + /// + /// Invalidates key and element pointers beyond the first deleted index. + pub fn orderedRemoveAtMany(self: *Self, gpa: Allocator, sorted_indexes: []const usize) Oom!void { + if (@sizeOf(ByIndexContext) != 0) + @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call orderedRemoveAtContext instead."); + return self.orderedRemoveAtManyContext(gpa, sorted_indexes, undefined); + } + + pub fn orderedRemoveAtManyContext( + self: *Self, + gpa: Allocator, + sorted_indexes: []const usize, + ctx: Context, + ) Oom!void { + self.pointer_stability.lock(); + defer self.pointer_stability.unlock(); + + self.entries.orderedRemoveMany(sorted_indexes); + try self.reIndexContext(gpa, ctx); + } + /// Create a copy of the hash map which can be modified separately. /// The copy uses the same context as this instance, but is allocated /// with the provided allocator. @@ -2660,3 +2687,29 @@ pub fn getAutoHashStratFn(comptime K: type, comptime Context: type, comptime str } }.hash; } + +test "orderedRemoveAtMany" { + const gpa = testing.allocator; + + var map: AutoArrayHashMapUnmanaged(usize, void) = .empty; + defer map.deinit(gpa); + + for (0..10) |n| { + try map.put(gpa, n, {}); + } + + try map.orderedRemoveAtMany(gpa, &.{ 1, 5, 5, 7, 9 }); + try testing.expectEqualSlices(usize, &.{ 0, 2, 3, 4, 6, 8 }, map.keys()); + + try map.orderedRemoveAtMany(gpa, &.{0}); + try testing.expectEqualSlices(usize, &.{ 2, 3, 4, 6, 8 }, map.keys()); + + try map.orderedRemoveAtMany(gpa, &.{}); + try testing.expectEqualSlices(usize, &.{ 2, 3, 4, 6, 8 }, map.keys()); + + try map.orderedRemoveAtMany(gpa, &.{ 1, 2, 3, 4 }); + try testing.expectEqualSlices(usize, &.{2}, map.keys()); + + try map.orderedRemoveAtMany(gpa, &.{0}); + try testing.expectEqualSlices(usize, &.{}, map.keys()); +} diff --git a/lib/std/array_list.zig b/lib/std/array_list.zig index 2a9159aeac71..53c9fc7294de 100644 --- a/lib/std/array_list.zig +++ b/lib/std/array_list.zig @@ -872,7 +872,6 @@ pub fn ArrayListAlignedUnmanaged(comptime T: type, comptime alignment: ?mem.Alig /// Remove the element at index `i` from the list and return its value. /// Invalidates pointers to the last element. /// This operation is O(N). - /// Asserts that the list is not empty. /// Asserts that the index is in bounds. pub fn orderedRemove(self: *Self, i: usize) T { const old_item = self.items[i]; @@ -880,6 +879,35 @@ pub fn ArrayListAlignedUnmanaged(comptime T: type, comptime alignment: ?mem.Alig return old_item; } + /// Remove the elements indexed by `sorted_indexes`. The indexes to be + /// removed correspond to the array list before deletion. + /// + /// Asserts: + /// * Each index to be removed is in bounds. + /// * The indexes to be removed are sorted ascending. + /// + /// Duplicates in `sorted_indexes` are allowed. + /// + /// This operation is O(N). + /// + /// Invalidates element pointers beyond the first deleted index. + pub fn orderedRemoveMany(self: *Self, sorted_indexes: []const usize) void { + if (sorted_indexes.len == 0) return; + var shift: usize = 1; + for (sorted_indexes[0 .. sorted_indexes.len - 1], sorted_indexes[1..]) |removed, end| { + if (removed == end) continue; // allows duplicates in `sorted_indexes` + const start = removed + 1; + const len = end - start; // safety checks `sorted_indexes` are sorted + @memmove(self.items[start - shift ..][0..len], self.items[start..][0..len]); // safety checks initial `sorted_indexes` are in range + shift += 1; + } + const start = sorted_indexes[sorted_indexes.len - 1] + 1; + const end = self.items.len; + const len = end - start; // safety checks final `sorted_indexes` are in range + @memmove(self.items[start - shift ..][0..len], self.items[start..][0..len]); + self.items.len = end - shift; + } + /// Removes the element at the specified index and returns it. /// The empty slot is filled from the end of the list. /// Invalidates pointers to last element. @@ -2248,3 +2276,29 @@ test "return OutOfMemory when capacity would exceed maximum usize integer value" try testing.expectError(error.OutOfMemory, list.ensureUnusedCapacity(2)); } } + +test "orderedRemoveMany" { + const gpa = testing.allocator; + + var list: ArrayListUnmanaged(usize) = .empty; + defer list.deinit(gpa); + + for (0..10) |n| { + try list.append(gpa, n); + } + + list.orderedRemoveMany(&.{ 1, 5, 5, 7, 9 }); + try testing.expectEqualSlices(usize, &.{ 0, 2, 3, 4, 6, 8 }, list.items); + + list.orderedRemoveMany(&.{0}); + try testing.expectEqualSlices(usize, &.{ 2, 3, 4, 6, 8 }, list.items); + + list.orderedRemoveMany(&.{}); + try testing.expectEqualSlices(usize, &.{ 2, 3, 4, 6, 8 }, list.items); + + list.orderedRemoveMany(&.{ 1, 2, 3, 4 }); + try testing.expectEqualSlices(usize, &.{2}, list.items); + + list.orderedRemoveMany(&.{0}); + try testing.expectEqualSlices(usize, &.{}, list.items); +} diff --git a/lib/std/multi_array_list.zig b/lib/std/multi_array_list.zig index 341ca6931efc..f22f55a492ed 100644 --- a/lib/std/multi_array_list.zig +++ b/lib/std/multi_array_list.zig @@ -334,6 +334,42 @@ pub fn MultiArrayList(comptime T: type) type { self.len -= 1; } + /// Remove the elements indexed by `sorted_indexes`. The indexes to be + /// removed correspond to the array list before deletion. + /// + /// Asserts: + /// * Each index to be removed is in bounds. + /// * The indexes to be removed are sorted ascending. + /// + /// Duplicates in `sorted_indexes` are allowed. + /// + /// This operation is O(N). + /// + /// Invalidates element pointers beyond the first deleted index. + pub fn orderedRemoveMany(self: *Self, sorted_indexes: []const usize) void { + if (sorted_indexes.len == 0) return; + const slices = self.slice(); + var shift: usize = 1; + for (sorted_indexes[0 .. sorted_indexes.len - 1], sorted_indexes[1..]) |removed, end| { + if (removed == end) continue; // allows duplicates in `sorted_indexes` + const start = removed + 1; + const len = end - start; // safety checks `sorted_indexes` are sorted + inline for (fields, 0..) |_, field_index| { + const field_slice = slices.items(@enumFromInt(field_index)); + @memmove(field_slice[start - shift ..][0..len], field_slice[start..][0..len]); // safety checks initial `sorted_indexes` are in range + } + shift += 1; + } + const start = sorted_indexes[sorted_indexes.len - 1] + 1; + const end = self.len; + const len = end - start; // safety checks final `sorted_indexes` are in range + inline for (fields, 0..) |_, field_index| { + const field_slice = slices.items(@enumFromInt(field_index)); + @memmove(field_slice[start - shift ..][0..len], field_slice[start..][0..len]); + } + self.len = end - shift; + } + /// Adjust the list's length to `new_len`. /// Does not initialize added items, if any. pub fn resize(self: *Self, gpa: Allocator, new_len: usize) !void { @@ -1008,3 +1044,29 @@ test "struct with many fields" { try ManyFields.doTest(testing.allocator, 100); try ManyFields.doTest(testing.allocator, 200); } + +test "orderedRemoveMany" { + const gpa = testing.allocator; + + var list: MultiArrayList(struct { x: usize }) = .empty; + defer list.deinit(gpa); + + for (0..10) |n| { + try list.append(gpa, .{ .x = n }); + } + + list.orderedRemoveMany(&.{ 1, 5, 5, 7, 9 }); + try testing.expectEqualSlices(usize, &.{ 0, 2, 3, 4, 6, 8 }, list.items(.x)); + + list.orderedRemoveMany(&.{0}); + try testing.expectEqualSlices(usize, &.{ 2, 3, 4, 6, 8 }, list.items(.x)); + + list.orderedRemoveMany(&.{}); + try testing.expectEqualSlices(usize, &.{ 2, 3, 4, 6, 8 }, list.items(.x)); + + list.orderedRemoveMany(&.{ 1, 2, 3, 4 }); + try testing.expectEqualSlices(usize, &.{2}, list.items(.x)); + + list.orderedRemoveMany(&.{0}); + try testing.expectEqualSlices(usize, &.{}, list.items(.x)); +}