Skip to content

Day 57: std.sparse_set模块:稀疏集

稀疏集(Sparse Set)是一种高度优化的数据结构,专门用于存储和操作整数集合,特别是当这些整数来自一个相对密集但可能很大的范围时。它在游戏开发中的实体组件系统(ECS)中非常流行,因为它提供了以下关键特性的完美结合:

  • O(1) 时间复杂度的插入、删除和查找操作。
  • 紧凑的、缓存友好的迭代,因为所有成员都连续存储。

std.SparseSet是Zig标准库对这种数据结构的实现,它为需要高性能集合操作的场景提供了强大的支持。

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
}

稀疏集的核心操作都具有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});
}
}

在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| {
// ... 对每个可移动实体执行操作
}

创建一个SparseSet来存储一组“活跃”的用户ID。编写一个函数,接收一个用户ID列表,并快速返回其中哪些ID是活跃的。

  • 索引范围和内存使用 sparse数组的大小由曾经插入到集合中的最大整数值决定。例如,如果你插入了1_000_000,即使集合中只有一个元素,sparse数组的长度也会增长到1_000_001。因此,稀疏集最适合处理那些虽然值可能很大,但最大值仍在可控范围内的整数。

  • 迭代顺序 SparseSet不保证迭代顺序。items()返回的切片中的元素顺序可能会因为插入和删除操作而改变。

  • 容量管理 dense数组和sparse数组都会在需要时自动扩容。与ArrayList类似,这些操作的成本在多次插入中被摊销。

SparseSet可以看作是std.BitSet的一个替代方案。当整数的范围非常大,以至于BitSet会消耗过多内存时,SparseSet就显示出其优势。例如,存储{1_000_000},BitSet需要大约125KB内存,而SparseSet只需要几个字节(如果这是唯一的元素)。它的O(1)复杂度和缓存友好的迭代使其成为高性能应用程序(尤其是游戏引擎和模拟)中不可或-缺的工具。