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
This commit is contained in:
Andrew Kelley 2025-05-18 12:45:30 -07:00
parent 282c3575b1
commit 59de7e3a57
3 changed files with 170 additions and 1 deletions

View File

@ -1273,6 +1273,33 @@ pub fn ArrayHashMapUnmanaged(
self.removeByIndex(index, if (store_hash) {} else ctx, .ordered); 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. /// Create a copy of the hash map which can be modified separately.
/// The copy uses the same context as this instance, but is allocated /// The copy uses the same context as this instance, but is allocated
/// with the provided allocator. /// with the provided allocator.
@ -2651,3 +2678,29 @@ pub fn getAutoHashStratFn(comptime K: type, comptime Context: type, comptime str
} }
}.hash; }.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());
}

View File

@ -935,7 +935,6 @@ pub fn ArrayListAlignedUnmanaged(comptime T: type, comptime alignment: ?mem.Alig
/// Remove the element at index `i` from the list and return its value. /// Remove the element at index `i` from the list and return its value.
/// Invalidates pointers to the last element. /// Invalidates pointers to the last element.
/// This operation is O(N). /// This operation is O(N).
/// Asserts that the list is not empty.
/// Asserts that the index is in bounds. /// Asserts that the index is in bounds.
pub fn orderedRemove(self: *Self, i: usize) T { pub fn orderedRemove(self: *Self, i: usize) T {
const old_item = self.items[i]; const old_item = self.items[i];
@ -943,6 +942,35 @@ pub fn ArrayListAlignedUnmanaged(comptime T: type, comptime alignment: ?mem.Alig
return old_item; 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. /// Removes the element at the specified index and returns it.
/// The empty slot is filled from the end of the list. /// The empty slot is filled from the end of the list.
/// Invalidates pointers to last element. /// Invalidates pointers to last element.
@ -2425,3 +2453,29 @@ test "return OutOfMemory when capacity would exceed maximum usize integer value"
try testing.expectError(error.OutOfMemory, list.ensureUnusedCapacity(2)); 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);
}

View File

@ -350,6 +350,42 @@ pub fn MultiArrayList(comptime T: type) type {
self.len -= 1; 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`. /// Adjust the list's length to `new_len`.
/// Does not initialize added items, if any. /// Does not initialize added items, if any.
pub fn resize(self: *Self, gpa: Allocator, new_len: usize) !void { pub fn resize(self: *Self, gpa: Allocator, new_len: usize) !void {
@ -1025,3 +1061,29 @@ test "struct with many fields" {
try ManyFields.doTest(testing.allocator, 100); try ManyFields.doTest(testing.allocator, 100);
try ManyFields.doTest(testing.allocator, 200); 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));
}