#链表 #向量 #列表 #链式

linked-vector

混合链表和向量的数据结构

12个版本 (4个稳定版本)

1.2.1 2023年2月13日
1.2.0 2023年2月12日
0.3.0 2023年2月8日
0.2.0 2023年2月8日
0.1.5 2023年2月7日

#951数据结构

每月46次 下载

MIT 许可证

98KB
2K SLoC

LinkedVector

LinkedVector 是一个功能丰富的向量和链表的混合体。项目可以在 O(1) 时间内直接访问,插入和删除操作也在 O(1) 时间内完成。在内部,节点存在于向量中,每个节点都持有对其前一个和后一个邻居的引用。因此,当插入或删除项目时,无需移动数据。

LFU 缓存示例

一个演示 linked-vector 遗留库使用的 示例项目 可用。该项目是一个最少使用缓存。使用 LinkedVector 来实现其频率计数队列。

更新

版本 v1.2.x 是对先前 v1.x.x 版本的微小修订,向后兼容。但是,用户必须显式启用 "cursor-remove" 功能。这会启用 CursorMut::remove() 方法。如果您之前没有使用 Cursor::remove(),则无需执行任何操作。否则,您可以将功能包含在 Cargo.toml 文件中,请参阅下面的使用说明

功能: "optionless-accessors"

版本 v1.2.0 添加了一个名为 "optionless-accessors" 的新功能,可以启用它,以对 LinkedVectorCursor 中的一些现有方法进行一些小的更改。鼓励启用此功能,因为它解决了某些API方法中的一些不合理方面。

启用此功能后,像 get(hnode)get_mut(hnode) 这样的方法会返回其值的直接引用,而不是 Option 变体。这些命令在错误的句柄上也会失败,所以返回一个 Option 没有意义。默认情况下禁用此功能以保持向后兼容性,但可以轻松启用,请参阅使用说明

功能:"cursor-remove"

LinkedVector API 禁止为空向量创建游标。如果您有一个向量的游标,则假定它有可遍历和/或修改的项目。移除项目可能会带来一些风险,因为在所有项目都被移除的情况下,游标对当前节点的内部引用将变得没有意义。

因此,为了确保用户意识到这一点,需要显式启用 "cursor-remove" 功能。要验证是否已通过游标清空向量,游标提供了一个 is_empty() 方法。此外,remove() 方法返回一个 Option,其中 None 表示没有更多项目可以移除。

版本约定

  • MAJOR 版本表示与先前主版本不兼容的 API 变更。
  • MINOR 版本表示以向后兼容的方式添加了功能。
  • PATCH 版本表示向后兼容的错误修复。

变更日志

使用方法

要使用 "optionless-accessors""cursor-remove" 功能,编辑您的 Cargo.toml 文件以包含

[dependencies]
linked-vector = { version = "1.2", features = ["cursor-remove", "optionless-accessors"] }

或者,要使用与现有 v1.1.0 代码向后兼容的 v1.2.0

[dependencies]
linked-vector = "1.2"

或者从您的项目文件夹中运行此命令行

cargo add linked-vector --features "cursor-remove, optionless-accessors"

或者不使用新功能

cargo add linked-vector

功能摘要

句柄

LinkedVector 中的项目可以通过句柄直接访问,句柄是 HNode 结构的实例。这些由插入或推送等操作返回,或其他访问器方法。如果需要直接访问任何特定项目,可以存储它们的句柄以供以后使用。这些句柄没有智能指针的性能开销,同时提供了一个灵活的引用模型。

use linked_vector::*;
let mut lv = LinkedVector::new();

let handle_1 = lv.push_back(1);
let handle_2 = lv.push_back(2);

lv[handle_1] = 42;
lv[handle_2] = 99;

assert_eq!(lv[handle_1], 42);
assert_eq!(lv[handle_2], 99);

回收

当节点被弹出或以其他方式移除时,它们会被添加到回收列表中。如果一个 LinkedVector 有任何节点在此列表中,其中一个将被用于下一个插入或推送操作。这种策略避免了将向量分割成无效的向量单元格。当节点被添加到回收列表中时,它不会被移动到向量中 - 它的下一个和前一个字段被更新以将其链接到回收列表。

调试功能

对于发布构建,本节中描述的检查被排除以确保快速性能。在发布版本中,句柄是 usize 索引到 LinkedVector 的内部向量的透明索引。

当以调试构建运行时,句柄将增加额外的字段:一个UUID字段和一个生成ID。UUID字段用于验证句柄是否属于传递给它们的LinkedVector。生成ID用于检测过期的句柄。

这些特性有助于确保使用此crate的项目在诸如将旧句柄传递给已弹出节点的向量,或从向量中获取句柄并意外传递到另一个向量等场景中不会出现难以追踪的bug。

经济性

LinkedVector的结构以极简方式实现。它只包含4个字段:一个用于内部向量,另一个用于存储指向头节点的句柄,另一个用于存储指向回收列表的句柄,最后是长度字段。

向量中没有虚拟节点 - 所有活动节点都是数据,而且LinkedVector结构中没有用于尾句柄的字段,尽管向量确实在O(1)时间内可以访问尾节点。

其他特性

  • 游标:Cursor接口方便从任何位置遍历向量。
  • 索引Index<HNode>Index<usize>得到了实现,允许直接访问项。
  • 迭代器:实现了标准的双端迭代器集合。
  • 排序:在O(n log n)时间内支持就地排序元素。

示例

句柄

修改LinkedVector的操作返回可以保存以供以后使用的句柄。这些提供直接访问项的能力,在O(1)时间内。

use linked_vector::*;
let mut lv = LinkedVector::new();

let h1 = lv.push_back(1);
let h2 = lv.push_back(2);
let h3 = lv.push_back(3);
let h4 = lv.insert_after(h1, 4);

lv.insert_after(h2, 42);
lv.remove(h1);

assert_eq!(lv.front(), Some(&4));
assert_eq!(lv.to_vec(), vec![4, 2, 42, 3]);

游标

可以从LinkedVector请求游标以方便遍历节点。使用句柄指定起始位置,游标可以设置到向量中的相应位置。它们可以一次移动一个位置,或者通过forward(n_times)backward(n_ntimes)移动多个位置。

use linked_vector::*;
let lv     = LinkedVector::from([1, 2, 3, 4, 5, 6, 7]);
let hfront = lv.front_node().unwrap();

let mut cursor = lv.cursor(hfront);

assert_eq!(*cursor, 1);

cursor.move_next();

assert_eq!(*cursor, 2);

let hend = cursor.move_to_end().expect("Moving to end");
let hbak = cursor.backward(3).expect("Moving back 3");

assert_eq!(*cursor, 4);
assert_eq!(lv[hend], 7);
assert_eq!(lv[hbak], 4);

迭代器

LinkedVector实现了标准的双端迭代器集。它们可以通过如iter()这样的方法直接实例化,或隐式实例化。

use linked_vector::*;
let mut lv1 = LinkedVector::from([1, 2, 3]);

lv1.iter_mut().zip(7..).for_each(|(a, b)| *a = b);
lv1.iter().zip(7..).for_each(|(a, b)| assert_eq!(a, &b));

for (v1, v2) in (10..).zip(&mut lv1) {
    *v2 = v1;
}
lv1.iter().zip(10..).for_each(|(a, b)| assert_eq!(a, &b));

依赖

~0.7–1.2MB
~25K SLoC