强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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

练习

  1. 使用 Table 实现一个简单的电话簿
  2. seqsequtils 实现数据过滤和统计
  3. 使用 set 实现权限位操作
  4. 实现一个简单的 LRU 缓存(使用 OrderedTable

扩展阅读


上一章:函数与过程 | 下一章:字符串处理