Day 57: std.sparse_set模块:稀疏集
1. 引言:高效的整数集合
Section titled “1. 引言:高效的整数集合”稀疏集(Sparse Set)是一种高度优化的数据结构,专门用于存储和操作整数集合,特别是当这些整数来自一个相对密集但可能很大的范围时。它在游戏开发中的实体组件系统(ECS)中非常流行,因为它提供了以下关键特性的完美结合:
- O(1) 时间复杂度的插入、删除和查找操作。
- 紧凑的、缓存友好的迭代,因为所有成员都连续存储。
std.SparseSet是Zig标准库对这种数据结构的实现,它为需要高性能集合操作的场景提供了强大的支持。
2. 定义:SparseSet(T)
Section titled “2. 定义:SparseSet(T)”std.SparseSet(T)是一个泛型结构体,其中T必须是一个无符号整数类型(如u16, u32等),代表集合中存储的元素。
它内部由两个数组组成:
sparse: 一个“稀疏”数组,其索引对应于可能存在于集合中的整数值。它存储的值是指向dense数组的索引。dense: 一个“密集”数组,连续地存储着集合中所有的成员。
const std = @import("std");
pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator();
// 创建一个可以存储u16类型整数的稀疏集 var my_set = std.SparseSet(u16).init(allocator); defer my_set.deinit();
// ... 使用my_set}3. 操作:insert, contains, remove
Section titled “3. 操作:insert, contains, remove”稀疏集的核心操作都具有O(1)的时间复杂度。
insert(item: T) !void: 将一个整数项添加到集合中。contains(item: T) bool: 检查一个整数项是否存在于集合中。remove(item: T) bool: 从集合中移除一个整数项。返回true如果该项存在并被移除。items(): 返回一个包含集合中所有项的切片,用于迭代。
const std = @import("std");
pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator();
var entities = std.SparseSet(u32).init(allocator); defer entities.deinit();
try entities.insert(100); try entities.insert(42); try entities.insert(999);
std.debug.print("Contains 42: {}\n", .{entities.contains(42)}); std.debug.print("Contains 50: {}\n", .{entities.contains(50)});
_ = entities.remove(42); std.debug.print("Contains 42 after removal: {}\n", .{entities.contains(42)});
// 迭代是紧凑的 for (entities.items()) |entity_id| { std.debug.print("Entity ID: {d}\n", .{entity_id}); }}4. 示例:实体组件系统(ECS)
Section titled “4. 示例:实体组件系统(ECS)”在ECS架构中,一个稀疏集可以用来跟踪所有拥有某个特定组件的实体ID。例如,一个positions稀疏集可以告诉我们哪些实体有位置组件。
// 假设Position组件存储在一个ArrayList中var positions = std.ArrayList(Vec3).init(allocator);
// `movable_entities`稀疏集跟踪哪些实体ID是可移动的var movable_entities = std.SparseSet(u32).init(allocator);
// 当给实体25添加一个Position组件时try positions.append(new_position);const pos_index = positions.len - 1;try movable_entities.insert(25);
// 我们可以快速检查实体25是否可移动if (movable_entities.contains(25)) { // ... 更新它的位置}
// 迭代所有可移动的实体非常高效for (movable_entities.items()) |entity_id| { // ... 对每个可移动实体执行操作}5. 实践练习:快速查找
Section titled “5. 实践练习:快速查找”创建一个SparseSet来存储一组“活跃”的用户ID。编写一个函数,接收一个用户ID列表,并快速返回其中哪些ID是活跃的。
6. 常见问题
Section titled “6. 常见问题”-
索引范围和内存使用
sparse数组的大小由曾经插入到集合中的最大整数值决定。例如,如果你插入了1_000_000,即使集合中只有一个元素,sparse数组的长度也会增长到1_000_001。因此,稀疏集最适合处理那些虽然值可能很大,但最大值仍在可控范围内的整数。 -
迭代顺序
SparseSet不保证迭代顺序。items()返回的切片中的元素顺序可能会因为插入和删除操作而改变。 -
容量管理
dense数组和sparse数组都会在需要时自动扩容。与ArrayList类似,这些操作的成本在多次插入中被摊销。
7. 总结:BitSet的替代方案
Section titled “7. 总结:BitSet的替代方案”SparseSet可以看作是std.BitSet的一个替代方案。当整数的范围非常大,以至于BitSet会消耗过多内存时,SparseSet就显示出其优势。例如,存储{1_000_000},BitSet需要大约125KB内存,而SparseSet只需要几个字节(如果这是唯一的元素)。它的O(1)复杂度和缓存友好的迭代使其成为高性能应用程序(尤其是游戏引擎和模拟)中不可或-缺的工具。