authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-05-18 12:45:30-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-08-11 13:32:12-07:00
log59de7e3a5725f0c4eeb64bc464db85e019af7446
tree02f14fd8a1601993ea41473f29ed4ef05f20e122
parent282c3575b16499518b19ee9b1a8364c9379dab94

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

3 files changed, 170 insertions(+), 1 deletions(-)

lib/std/array_hash_map.zig+53
......@@ -1273,6 +1273,33 @@ pub fn ArrayHashMapUnmanaged(
12731273 self.removeByIndex(index, if (store_hash) {} else ctx, .ordered);
12741274 }
12751275
1276 /// Remove the entries indexed by `sorted_indexes`. The indexes to be
1277 /// removed correspond to state before deletion.
1278 ///
1279 /// This operation is O(N).
1280 ///
1281 /// Asserts that each index to be removed is in bounds.
1282 ///
1283 /// Invalidates key and element pointers beyond the first deleted index.
1284 pub fn orderedRemoveAtMany(self: *Self, gpa: Allocator, sorted_indexes: []const usize) Oom!void {
1285 if (@sizeOf(ByIndexContext) != 0)
1286 @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call orderedRemoveAtContext instead.");
1287 return self.orderedRemoveAtManyContext(gpa, sorted_indexes, undefined);
1288 }
1289
1290 pub fn orderedRemoveAtManyContext(
1291 self: *Self,
1292 gpa: Allocator,
1293 sorted_indexes: []const usize,
1294 ctx: Context,
1295 ) Oom!void {
1296 self.pointer_stability.lock();
1297 defer self.pointer_stability.unlock();
1298
1299 self.entries.orderedRemoveMany(sorted_indexes);
1300 try self.reIndexContext(gpa, ctx);
1301 }
1302
12761303 /// Create a copy of the hash map which can be modified separately.
12771304 /// The copy uses the same context as this instance, but is allocated
12781305 /// with the provided allocator.
......@@ -2651,3 +2678,29 @@ pub fn getAutoHashStratFn(comptime K: type, comptime Context: type, comptime str
26512678 }
26522679 }.hash;
26532680}
2681
2682test "orderedRemoveAtMany" {
2683 const gpa = testing.allocator;
2684
2685 var map: AutoArrayHashMapUnmanaged(usize, void) = .empty;
2686 defer map.deinit(gpa);
2687
2688 for (0..10) |n| {
2689 try map.put(gpa, n, {});
2690 }
2691
2692 try map.orderedRemoveAtMany(gpa, &.{ 1, 5, 5, 7, 9 });
2693 try testing.expectEqualSlices(usize, &.{ 0, 2, 3, 4, 6, 8 }, map.keys());
2694
2695 try map.orderedRemoveAtMany(gpa, &.{0});
2696 try testing.expectEqualSlices(usize, &.{ 2, 3, 4, 6, 8 }, map.keys());
2697
2698 try map.orderedRemoveAtMany(gpa, &.{});
2699 try testing.expectEqualSlices(usize, &.{ 2, 3, 4, 6, 8 }, map.keys());
2700
2701 try map.orderedRemoveAtMany(gpa, &.{ 1, 2, 3, 4 });
2702 try testing.expectEqualSlices(usize, &.{2}, map.keys());
2703
2704 try map.orderedRemoveAtMany(gpa, &.{0});
2705 try testing.expectEqualSlices(usize, &.{}, map.keys());
2706}
lib/std/array_list.zig+55-1
......@@ -935,7 +935,6 @@ pub fn ArrayListAlignedUnmanaged(comptime T: type, comptime alignment: ?mem.Alig
935935 /// Remove the element at index `i` from the list and return its value.
936936 /// Invalidates pointers to the last element.
937937 /// This operation is O(N).
938 /// Asserts that the list is not empty.
939938 /// Asserts that the index is in bounds.
940939 pub fn orderedRemove(self: *Self, i: usize) T {
941940 const old_item = self.items[i];
......@@ -943,6 +942,35 @@ pub fn ArrayListAlignedUnmanaged(comptime T: type, comptime alignment: ?mem.Alig
943942 return old_item;
944943 }
945944
945 /// Remove the elements indexed by `sorted_indexes`. The indexes to be
946 /// removed correspond to the array list before deletion.
947 ///
948 /// Asserts:
949 /// * Each index to be removed is in bounds.
950 /// * The indexes to be removed are sorted ascending.
951 ///
952 /// Duplicates in `sorted_indexes` are allowed.
953 ///
954 /// This operation is O(N).
955 ///
956 /// Invalidates element pointers beyond the first deleted index.
957 pub fn orderedRemoveMany(self: *Self, sorted_indexes: []const usize) void {
958 if (sorted_indexes.len == 0) return;
959 var shift: usize = 1;
960 for (sorted_indexes[0 .. sorted_indexes.len - 1], sorted_indexes[1..]) |removed, end| {
961 if (removed == end) continue; // allows duplicates in `sorted_indexes`
962 const start = removed + 1;
963 const len = end - start; // safety checks `sorted_indexes` are sorted
964 @memmove(self.items[start - shift ..][0..len], self.items[start..][0..len]); // safety checks initial `sorted_indexes` are in range
965 shift += 1;
966 }
967 const start = sorted_indexes[sorted_indexes.len - 1] + 1;
968 const end = self.items.len;
969 const len = end - start; // safety checks final `sorted_indexes` are in range
970 @memmove(self.items[start - shift ..][0..len], self.items[start..][0..len]);
971 self.items.len = end - shift;
972 }
973
946974 /// Removes the element at the specified index and returns it.
947975 /// The empty slot is filled from the end of the list.
948976 /// Invalidates pointers to last element.
......@@ -2425,3 +2453,29 @@ test "return OutOfMemory when capacity would exceed maximum usize integer value"
24252453 try testing.expectError(error.OutOfMemory, list.ensureUnusedCapacity(2));
24262454 }
24272455}
2456
2457test "orderedRemoveMany" {
2458 const gpa = testing.allocator;
2459
2460 var list: ArrayListUnmanaged(usize) = .empty;
2461 defer list.deinit(gpa);
2462
2463 for (0..10) |n| {
2464 try list.append(gpa, n);
2465 }
2466
2467 list.orderedRemoveMany(&.{ 1, 5, 5, 7, 9 });
2468 try testing.expectEqualSlices(usize, &.{ 0, 2, 3, 4, 6, 8 }, list.items);
2469
2470 list.orderedRemoveMany(&.{0});
2471 try testing.expectEqualSlices(usize, &.{ 2, 3, 4, 6, 8 }, list.items);
2472
2473 list.orderedRemoveMany(&.{});
2474 try testing.expectEqualSlices(usize, &.{ 2, 3, 4, 6, 8 }, list.items);
2475
2476 list.orderedRemoveMany(&.{ 1, 2, 3, 4 });
2477 try testing.expectEqualSlices(usize, &.{2}, list.items);
2478
2479 list.orderedRemoveMany(&.{0});
2480 try testing.expectEqualSlices(usize, &.{}, list.items);
2481}
lib/std/multi_array_list.zig+62
......@@ -350,6 +350,42 @@ pub fn MultiArrayList(comptime T: type) type {
350350 self.len -= 1;
351351 }
352352
353 /// Remove the elements indexed by `sorted_indexes`. The indexes to be
354 /// removed correspond to the array list before deletion.
355 ///
356 /// Asserts:
357 /// * Each index to be removed is in bounds.
358 /// * The indexes to be removed are sorted ascending.
359 ///
360 /// Duplicates in `sorted_indexes` are allowed.
361 ///
362 /// This operation is O(N).
363 ///
364 /// Invalidates element pointers beyond the first deleted index.
365 pub fn orderedRemoveMany(self: *Self, sorted_indexes: []const usize) void {
366 if (sorted_indexes.len == 0) return;
367 const slices = self.slice();
368 var shift: usize = 1;
369 for (sorted_indexes[0 .. sorted_indexes.len - 1], sorted_indexes[1..]) |removed, end| {
370 if (removed == end) continue; // allows duplicates in `sorted_indexes`
371 const start = removed + 1;
372 const len = end - start; // safety checks `sorted_indexes` are sorted
373 inline for (fields, 0..) |_, field_index| {
374 const field_slice = slices.items(@enumFromInt(field_index));
375 @memmove(field_slice[start - shift ..][0..len], field_slice[start..][0..len]); // safety checks initial `sorted_indexes` are in range
376 }
377 shift += 1;
378 }
379 const start = sorted_indexes[sorted_indexes.len - 1] + 1;
380 const end = self.len;
381 const len = end - start; // safety checks final `sorted_indexes` are in range
382 inline for (fields, 0..) |_, field_index| {
383 const field_slice = slices.items(@enumFromInt(field_index));
384 @memmove(field_slice[start - shift ..][0..len], field_slice[start..][0..len]);
385 }
386 self.len = end - shift;
387 }
388
353389 /// Adjust the list's length to `new_len`.
354390 /// Does not initialize added items, if any.
355391 pub fn resize(self: *Self, gpa: Allocator, new_len: usize) !void {
......@@ -1025,3 +1061,29 @@ test "struct with many fields" {
10251061 try ManyFields.doTest(testing.allocator, 100);
10261062 try ManyFields.doTest(testing.allocator, 200);
10271063}
1064
1065test "orderedRemoveMany" {
1066 const gpa = testing.allocator;
1067
1068 var list: MultiArrayList(struct { x: usize }) = .empty;
1069 defer list.deinit(gpa);
1070
1071 for (0..10) |n| {
1072 try list.append(gpa, .{ .x = n });
1073 }
1074
1075 list.orderedRemoveMany(&.{ 1, 5, 5, 7, 9 });
1076 try testing.expectEqualSlices(usize, &.{ 0, 2, 3, 4, 6, 8 }, list.items(.x));
1077
1078 list.orderedRemoveMany(&.{0});
1079 try testing.expectEqualSlices(usize, &.{ 2, 3, 4, 6, 8 }, list.items(.x));
1080
1081 list.orderedRemoveMany(&.{});
1082 try testing.expectEqualSlices(usize, &.{ 2, 3, 4, 6, 8 }, list.items(.x));
1083
1084 list.orderedRemoveMany(&.{ 1, 2, 3, 4 });
1085 try testing.expectEqualSlices(usize, &.{2}, list.items(.x));
1086
1087 list.orderedRemoveMany(&.{0});
1088 try testing.expectEqualSlices(usize, &.{}, list.items(.x));
1089}