Skip to content

Commit 8f56d9d

Browse files
committed
docs: fix terminology
1 parent 2489f1c commit 8f56d9d

File tree

2 files changed

+8
-6
lines changed

2 files changed

+8
-6
lines changed

trees/blog/fenwick/introduction.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ collect: true
2222

2323
我们可以通过引入额外的缓存层级,以略微增加更新操作的开销为代价,进一步加快区间查询的速度。例如,我们可以将序列划分为 $\sqrt[3]{n}$ 个“大桶”,然后将每个大桶进一步细分为 $\sqrt[3]{n}$ 个“小桶”,每个小桶包含 $\sqrt[3]{n}$ 个值。每个桶的和都被缓存;现在每次更新需要修改三个值,而区间查询则在 $O(\sqrt[3]{n})$ 时间内完成。
2424

25-
以此类推,我们最终会得到一种基于二分治策略的区间和缓存方法,使得更新和区间查询操作的时间复杂度均为 $O(\lg n)$。具体而言,我们可以构建一棵平衡二叉树,其叶节点存储序列本身,而每个内部节点则存储其子节点的和。(这对于许多函数式程序员而言应是一个耳熟能详的概念;例如,手指树(finger trees)(Hinze & Paterson, 2006; Apfelmus, 2009)便采用了类似的缓存机制。)由此产生的数据结构通常被称为线段树,[^2] 大抵是因为每个内部节点最终都缓存了底层序列的一个(连续)段的和。
25+
以此类推,我们最终会得到一种基于二分治策略的区间和缓存方法,使得更新和区间查询操作的时间复杂度均为[^2] $O(\log n)$ 。具体而言,我们可以构建一棵完全二叉树[^3],其叶节点存储序列本身,而每个内部节点则存储其子节点的和。(这对于许多函数式程序员而言应是一个耳熟能详的概念;例如,手指树(finger trees)(Hinze & Paterson, 2006; Apfelmus, 2009)便采用了类似的缓存机制。)由此产生的数据结构通常被称为线段树,[^4] 大抵是因为每个内部节点最终都缓存了底层序列的一个(连续)段的和。
2626

2727
<figure><img src="https://static.cambridge.org/binary/version/id/urn:cambridge.org:id:binary:20250116174732089-0720:S0956796824000169:S0956796824000169_fig2.png" width="80%"><figcaption>图 2: 线段树</figcaption></figure>
2828

2929
图 2 展示了一棵基于长度为 $n=16$ 的示例数组构建的线段树(为简化起见,我们假设 $n$ 是 2 的幂,尽管将其推广到非 2 的幂的情况亦不难)。树的每个叶节点对应数组中的一个元素;每个内部节点都带有一个灰色条块,显示其所代表的底层数组段。
3030

3131
我们来看如何利用线段树来实现前面所述的两种操作,并使其均能在对数时间内完成。
3232

33-
- 要更新索引 $i$ 处的值,我们还需要更新包含该值的所有缓存区间和。这些恰好是沿着从索引 $i$ 处的叶节点到树根路径上的所有节点;这样的节点数量级为 $O(\lg n)$。图 3 阐释了图 2 中示例线段树的更新过程;更新索引 5 处的条目仅需修改根节点到被更新条目路径上的阴影节点。
33+
- 要更新索引 $i$ 处的值,我们还需要更新包含该值的所有缓存区间和。这些恰好是沿着从索引 $i$ 处的叶节点到树根路径上的所有节点;这样的节点数量级为 $O(\log n)$。图 3 阐释了图 2 中示例线段树的更新过程;更新索引 5 处的条目仅需修改根节点到被更新条目路径上的阴影节点。
3434
- 要执行区间查询,我们自顶向下遍历树,同时记录当前节点所覆盖的范围。
3535
- 若当前节点的范围完全包含在查询范围内,则返回当前节点的值。
3636
- 若当前节点的范围与查询范围不相交,则返回 0。
@@ -42,7 +42,7 @@ collect: true
4242

4343
<figure><img src="https://static.cambridge.org/binary/version/id/urn:cambridge.org:id:binary:20250116174732089-0720:S0956796824000169:S0956796824000169_fig4.png" width="80%"><figcaption>图 4: 在线段树上进行区间查询</figcaption></figure>
4444

45-
在这个小例子中,看似我们访问了相当大比例的节点,但一般而言,我们访问的节点数量不会超过大约 $4 \lg n$ 个。图 5 更清晰地展示了这一点。整棵树中最多只有一个蓝色节点可以拥有两个蓝色子节点,因此,树的每一层最多包含两个蓝色节点和两个非蓝色节点。我们实际上执行了两次二分查找,一次用于寻找查询区间的左端点,另一次用于寻找右端点。
45+
在这个小例子中,看似我们访问了相当大比例的节点,但一般而言,我们访问的节点数量不会超过大约 $4 \log n$ 个。图 5 更清晰地展示了这一点。整棵树中最多只有一个蓝色节点可以拥有两个蓝色子节点,因此,树的每一层最多包含两个蓝色节点和两个非蓝色节点。我们实际上执行了两次二分查找,一次用于寻找查询区间的左端点,另一次用于寻找右端点。
4646

4747
<figure><img src="https://static.cambridge.org/binary/version/id/urn:cambridge.org:id:binary:20250116174732089-0720:S0956796824000169:S0956796824000169_fig5.png" width="80%"><figcaption>图 5: 在更大的线段树上进行区间查询</figcaption></figure>
4848

@@ -55,4 +55,6 @@ collect: true
5555
我们的目标,并非为此已解决的问题编写优雅的函数式代码。相反,我们的目标在于运用一种函数式的领域特定语言来处理位串,并结合等式推理,从第一性原理出发,推导并阐释这段令人费解的命令式代码——展示函数式思维与等式推理之力,即便用于理解以其他非函数式语言编写之代码亦能奏效。在对线段树(第 2 节)建立更深入的直觉之后,我们将看到芬威克树如何能被视为线段树的一种变体(第 3 节)。随后,我们将绕道探讨二进制补码表示法,开发一个适宜的位操作 DSL,并解释`LSB`函数的实现(第 4 节)。借助于该 DSL,我们将推导出在芬威克树与标准二叉树之间来回转换的函数(第 5 节)。最终,我们将能够推导出在芬威克树内部移动的函数:先将其转换为二叉树索引,执行显而易见的操作以实现二叉树内的预期移动,然后再转换回来。通过等式推理将转换过程融合消除,最终将揭示隐藏其后的`LSB`函数,一如预期(第 6 节)。
5656

5757
[^1]: 值得注意的是,本文及后续部分均采用基于 1 的索引方式,即序列中的首个元素索引为 1。此选择之缘由,后文将予以阐明。
58-
[^2]: 此处术语的使用存在一些混淆。截至本文撰写之时,维基百科关于线段树(Wikipedia Contributors, 2024)的条目讨论的是一种用于计算几何的区间数据结构。然而,谷歌搜索“segment tree”的大部分结果都来自算法竞赛领域,其中它指的是本文所讨论的数据结构(例如,参见 Halim et al., 2020, Section 2.8 或 Ivanov, 2011b)。这两种数据结构基本上是不相关的。
58+
[^2]: 本文的 $\log$ 表示以 2 为底的对数,原文使用 $\lg$。
59+
[^3]: 原文是 *balanced binary tree*,翻译为平衡二叉树或有歧义,此处使用完全二叉树以避免歧义。
60+
[^4]: 此处术语的使用存在一些混淆。截至本文撰写之时,维基百科关于线段树(Wikipedia Contributors, 2024)的条目讨论的是一种用于计算几何的区间数据结构。然而,谷歌搜索“segment tree”的大部分结果都来自算法竞赛领域,其中它指的是本文所讨论的数据结构(例如,参见 Halim et al., 2020, Section 2.8 或 Ivanov, 2011b)。这两种数据结构基本上是不相关的。

trees/blog/fenwick/two_complement.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ collect: true
88
首先,我们定义一个位 (Bit) 的类型,附带用于求反、逻辑与和逻辑或的函数(注:还有 `to_enum``from_enum` 的实现,用于实现和 `Int` 类型的互转,在此省略):
99
![bit](moonbit/src//fenwick/bit.mbt#:include)
1010

11-
接下来,我们必须定义位串,即位的序列。与其固定一个特定的位宽,不如使用无限位串会更加优雅[^3]。使用 [Lazy List](https://github.com/CAIMEOX/lazy-list) 来表示潜在的无限位串似乎颇具诱惑力,但这会导致一系列问题。例如,无限列表的相等性是不可判定的,而且通常没有办法将一个无限的位列表转换回一个 `Int` —— 我们如何知道何时停止?(译者注:Lazy List 在 MoonBit 中也容易出现栈溢出的问题,比原文使用的 Haskell `List` 还要更坏一些) 事实上,这些实际问题源于一个更根本的问题:无限位列表对于二进制补码位串来说是一个糟糕的表示,因为它包含“垃圾”,即那些不对应于我们预期语义域中值的无限位列表。例如,`cycle([I, O])` 是一个永远在 `I` 和 `O` 之间交替的无限列表,但它并不代表一个有效的二进制补码整数编码。更糟糕的是非周期列表,比如在每个素数索引处为 `I` 而其他地方均为 `O` 的列表。
11+
接下来,我们必须定义位串,即位的序列。与其固定一个特定的位宽,不如使用无限位串会更加优雅[^5]。使用 [Lazy List](https://github.com/CAIMEOX/lazy-list) 来表示潜在的无限位串似乎颇具诱惑力,但这会导致一系列问题。例如,无限列表的相等性是不可判定的,而且通常没有办法将一个无限的位列表转换回一个 `Int` —— 我们如何知道何时停止?(译者注:Lazy List 在 MoonBit 中也容易出现栈溢出的问题,比原文使用的 Haskell `List` 还要更坏一些) 事实上,这些实际问题源于一个更根本的问题:无限位列表对于二进制补码位串来说是一个糟糕的表示,因为它包含“垃圾”,即那些不对应于我们预期语义域中值的无限位列表。例如,`cycle([I, O])` 是一个永远在 `I` 和 `O` 之间交替的无限列表,但它并不代表一个有效的二进制补码整数编码。更糟糕的是非周期列表,比如在每个素数索引处为 `I` 而其他地方均为 `O` 的列表。
1212

1313
实际上,我们想要的位串是那些最终恒定的串,即那些最终趋于无限长的全零尾部(代表非负整数)或全一尾部(代表负整数)的串。每个这样的串都有一个有限的表示,因此在 MoonBit 中直接编码最终恒定的位串,不仅能去除垃圾,还能辅助编写一些优雅且停机的算法。
1414

@@ -82,4 +82,4 @@ trait `Show` 的实现:
8282

8383
![shift](moonbit/src//fenwick/bits.mbt#:include)
8484

85-
[^3]: 部分读者或许能认出无限二进制补码位串即为二进数,也即 p 进数在特定情况 $p=2$ 时的形式,但我们的论述并不依赖于理解此关联。
85+
[^5]: 部分读者或许能认出无限二进制补码位串即为二进数,也即 p 进数在特定情况 $p=2$ 时的形式,但我们的论述并不依赖于理解此关联。

0 commit comments

Comments
 (0)