Nim 完全指南 / 08 数据结构
第 08 章:数据结构
8.1 元组(Tuple)
元组是固定大小、固定类型的值集合:
# 类型定义
type
Point = tuple[x, y: float]
Person = tuple[name: string, age: int]
# 创建元组
let p: Point = (1.0, 2.0)
echo p.x, ", ", p.y # 1.0, 2.0
let person: Person = ("张三", 28)
echo person.name # 张三
# 匿名元组
let pair = (42, "hello")
echo pair[0] # 42
echo pair[1] # hello
# 解构
let (x, y) = (1.0, 2.0)
echo x, ", ", y
let (name, age) = person
echo &"{name}, {age}岁"
# 元组是值类型
var a = (1, 2)
var b = a
b[0] = 10
echo a # (1, 2) -- 不受影响
8.2 序列(Sequence)
序列是 Nim 中最常用的动态数组,类似 Python 的 list:
8.2.1 创建序列
# 使用 @[] 字面量
let nums = @[1, 2, 3, 4, 5]
let names = @["Alice", "Bob", "Charlie"]
# 创建空序列(必须指定类型)
var empty: seq[int] = @[]
var empty2 = newSeq[int]()
# 创建并初始化
var zeros = newSeq[int](10) # 10 个零
var filled = newSeqWith(5, "hi") # 5 个 "hi"
# 使用 sequtils 创建
import std/sequtils
let squares = toSeq(1..10).mapIt(it * it)
echo squares # @[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
8.2.2 序列操作
var s = @[10, 20, 30]
# 添加元素
s.add(40) # 尾部添加
s.insert(15, 1) # 在索引 1 处插入
s &= @[50, 60] # 连接序列
# 删除元素
s.delete(0) # 删除索引 0
s.del(0) # 无序删除(更快,不保持顺序)
s.pop() # 移除并返回最后一个元素
# 访问元素
echo s[0] # 第一个元素
echo s[^1] # 最后一个元素(^1 = len-1)
echo s[^2] # 倒数第二个
# 安全访问
echo s.getOrDefault(0) # 越界时返回默认值
echo s.getOrDefault(100, -1) # 指定默认值
# 切片
let data = @[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
echo data[2..5] # @[2, 3, 4, 5](包含右端点)
echo data[2..<5] # @[2, 3, 4](不包含右端点)
echo data[3..^1] # @[3, 4, 5, 6, 7, 8, 9]
# 修改切片
var m = @[0, 1, 2, 3, 4, 5]
m[1..3] = @[10, 20, 30]
echo m # @[0, 10, 20, 30, 4, 5]
# 包含检查
echo 20 in s # true
echo 100 notin s # true
# 查找位置
echo s.find(20) # 索引,-1 表示未找到
echo s.contains(20) # true
8.2.3 序列高级操作
import std/[sequtils, algorithm, sugar]
let nums = @[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
# 排序
var sorted_nums = nums
sorted_nums.sort() # 原地排序
echo sorted_nums # @[1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]
let desc = nums.sorted(SortOrder.Descending)
echo desc # @[9, 6, 5, 5, 5, 4, 3, 3, 2, 1, 1]
# 自定义排序
let words = @["banana", "apple", "cherry"]
let byLen = words.sortedByIt(it.len)
echo byLen # @["apple", "banana", "cherry"]
# 过滤
let evens = nums.filterIt(it mod 2 == 0)
echo evens # @[4, 2, 6]
# 映射
let doubled = nums.mapIt(it * 2)
echo doubled # @[6, 2, 8, 2, 10, ...]
# 聚合
echo nums.foldl(a + b, 0) # 求和: 44
echo nums.foldl(max(a, b)) # 最大值: 9
# 去重
echo nums.deduplicate() # @[3, 1, 4, 5, 9, 2, 6]
# 分组
let grouped = nums.distribute(3) # 均分为3组
for g in grouped:
echo g
# 压缩
let a = @[1, 2, 3]
let b = @["a", "b", "c"]
let zipped = zip(a, b)
echo zipped # @[(1, "a"), (2, "b"), (3, "c")]
# 展平
let nested = @[@[1, 2], @[3, 4], @[5]]
echo nested.concat() # @[1, 2, 3, 4, 5]
# 全部满足/任意满足
echo nums.allIt(it > 0) # true
echo nums.anyIt(it > 8) # true
8.3 集合(Set)
8.3.1 基本集合操作
# 创建集合
var s1 = {1, 2, 3, 4, 5}
var s2 = {4, 5, 6, 7, 8}
# 添加/删除元素
s1.incl(6)
s1.excl(3)
# 集合运算
echo s1 + s2 # 并集: {1, 2, 4, 5, 6, 7, 8}
echo s1 * s2 # 交集: {4, 5, 6}
echo s1 - s2 # 差集: {1, 2}
echo s1 -+- s2 # 对称差集: {1, 2, 7, 8}
# 子集/超集
echo {1, 2} <= s1 # true(子集)
echo s1 >= {1, 2} # true(超集)
echo s1 < s1 # false(真子集)
8.3.2 HashSet(大型集合)
set[T] 只能用于 range[0..65535] 或 enum。对于更通用的集合,使用 HashSet:
import std/sets
var hs = toHashSet([1, 2, 3, 4, 5])
hs.incl(6)
hs.excl(3)
echo hs.contains(4) # true
echo hs.len # 5
# 集合运算
let a = toHashSet([1, 2, 3, 4])
let b = toHashSet([3, 4, 5, 6])
echo a + b # 并集
echo a * b # 交集
echo a - b # 差集
# 字符串集合
var words = initHashSet[string]()
words.incl("hello")
words.incl("world")
echo "hello" in words # true
8.4 表(Table)
8.4.1 基本 Table
import std/tables
# 创建表
var scores = initTable[string, int]()
scores["张三"] = 95
scores["李四"] = 87
scores["王五"] = 92
# 字面量创建
let capitals = {"中国": "北京", "日本": "东京", "韩国": "首尔"}.toTable
# 访问
echo scores["张三"] # 95
echo scores.getOrDefault("赵六", 0) # 0(不存在时返回默认值)
# 检查键是否存在
echo scores.hasKey("张三") # true
echo "李四" in scores # true
echo "赵六" notin scores # true
# 添加/修改
scores["赵六"] = 78 # 添加
scores["张三"] = 98 # 修改
# 删除
scores.del("李四")
# 遍历
for name, score in scores:
echo &"{name}: {score}分"
8.4.2 OrderedTable(有序表)
import std/tables
# OrderedTable 保持插入顺序
var ordered = initOrderedTable[string, int]()
ordered["c"] = 3
ordered["a"] = 1
ordered["b"] = 2
for key, val in ordered:
echo &"{key}: {val}"
# c: 3
# a: 1
# b: 2
8.4.3 CountTable(计数表)
import std/tables
# 统计词频
let text = "hello world hello nim hello world"
let words = text.split(" ")
var counts = initCountTable[string]()
for word in words:
counts.inc(word)
echo counts # {"hello": 3, "world": 2, "nim": 1}
echo counts["hello"] # 3
# 最常见的 N 个
for word, count in counts.largest(2):
echo &"{word}: {count}"
8.4.4 TableRef(引用语义表)
import std/tables
# TableRef 是引用类型,适合共享和传递
type Config = TableRef[string, string]
proc newConfig(): Config =
newTable[string, string]()
proc setConfig(cfg: Config, key, value: string) =
cfg[key] = value
proc getConfig(cfg: Config, key: string, default = ""): string =
cfg.getOrDefault(key, default)
var cfg = newConfig()
cfg.setConfig("host", "localhost")
cfg.setConfig("port", "8080")
echo cfg.getConfig("host") # localhost
echo cfg.getConfig("port") # 8080
8.5 数组(Array)
数组是固定大小的序列,长度在编译期确定:
# 固定大小数组
var arr: array[5, int] = [1, 2, 3, 4, 5]
echo arr[0] # 1
echo arr.len # 5
# 范围索引数组
type
Day = enum Mon, Tue, Wed, Thu, Fri, Sat, Sun
WeekSchedule = array[Day, string]
var schedule: WeekSchedule
schedule[Mon] = "开会"
schedule[Tue] = "编程"
schedule[Wed] = "编程"
schedule[Thu] = "代码评审"
schedule[Fri] = "总结"
schedule[Sat] = "休息"
schedule[Sun] = "休息"
for day in Day:
echo &"{day}: {schedule[day]}"
# 多维数组
type Matrix3x3 = array[3, array[3, float]]
var m: Matrix3x3 = [
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0]
]
echo m[1][1] # 1.0
8.6 数据结构选择指南
| 需求 | 推荐类型 | 原因 |
|---|---|---|
| 动态数组 | seq[T] | 可变长度,O(1) 末尾追加 |
| 固定数组 | array[N, T] | 编译期大小,栈分配 |
| 键值映射 | Table[K, V] | O(1) 查找 |
| 有序键值 | OrderedTable[K, V] | 保持插入顺序 |
| 计数统计 | CountTable[K] | 内置计数功能 |
| 去重集合 | HashSet[T] | O(1) 查找,支持大范围类型 |
| 小范围集合 | set[T] | 极高效,位图实现 |
| 固定字段 | tuple | 值类型,编译期字段名 |
8.7 实战示例
🏢 场景:学生成绩管理系统
import std/[tables, algorithm, strformat]
type
Student = object
name: string
scores: seq[float]
GradeBook = object
students: Table[string, Student]
proc newGradeBook(): GradeBook =
GradeBook(students: initTable[string, Student]())
proc addStudent(gb: var GradeBook, id, name: string) =
gb.students[id] = Student(name: name, scores: @[])
proc addScore(gb: var GradeBook, id: string, score: float) =
if gb.students.hasKey(id):
gb.students[id].scores.add(score)
proc average(s: Student): float =
if s.scores.len == 0: 0.0
else: s.scores.sum() / s.scores.len.float
proc report(gb: GradeBook) =
echo "=" .repeat(50)
echo &"{'ID':<8} {'姓名':<10} {'分数':<20} {'平均':>6}"
echo "-".repeat(50)
for id, student in gb.students:
let scoresStr = student.scores.mapIt(&"{it:.0f}").join(", ")
echo &"{id:<8} {student.name:<10} {scoresStr:<20} {student.average():>6.1f}"
echo "=" .repeat(50)
var gb = newGradeBook()
gb.addStudent("S001", "张三")
gb.addStudent("S002", "李四")
gb.addStudent("S003", "王五")
gb.addScore("S001", 95); gb.addScore("S001", 88); gb.addScore("S001", 92)
gb.addScore("S002", 78); gb.addScore("S002", 85); gb.addScore("S002", 90)
gb.addScore("S003", 92); gb.addScore("S003", 96); gb.addScore("S003", 89)
gb.report()
输出:
==================================================
ID 姓名 分数 平均
--------------------------------------------------
S001 张三 95, 88, 92 91.7
S002 李四 78, 85, 90 84.3
S003 王五 92, 96, 89 92.3
==================================================
本章小结
| 数据结构 | 类型 | 语法 |
|---|---|---|
| 元组 | tuple | (1, "a") |
| 序列 | seq[T] | @[1, 2, 3] |
| 数组 | array[N, T] | [1, 2, 3] |
| 集合 | set[T] | {1, 2, 3} |
| 哈希集 | HashSet[T] | toHashSet([1, 2]) |
| 表 | Table[K, V] | {"a": 1}.toTable |
练习
- 使用
Table实现一个简单的电话簿 - 用
seq和sequtils实现数据过滤和统计 - 使用
set实现权限位操作 - 实现一个简单的 LRU 缓存(使用
OrderedTable)