设n 为数组的长度,k 为切片的长度。
天真的,O(log k),方法
一个明显的解决方案是构建一棵树,重复给出最大值的成对汇总
1 8 4 5 4 0 1 5 6 9 1 7 0 4 0 9 0 7 0 4 5 7 4 3 4 6 3 8 2 4 · ·
8 5 4 5 9 7 4 9 7 4 7 4 6 8 4 ·
8 5 9 9 7 7 8 4
8 9 7 8
9 8
9
这些摘要最多占用O(n) 空间,并且可以通过使用短索引有效地存储较低级别。例如,底层可以是位数组。追加和单一突变需要O(log n) 时间。如果需要,还有许多其他方面需要优化。
所选切片可以分成两个切片,在两个三角形之间的边界上拆分。在此示例中,对于给定的切片,我们将这样拆分:
|---------------------------------|
6 9 1 7 0 4 0 9|0 7 0 4 5 7 4 3 4 6 3 8 2 4 · ·
9 7 4 9 | 7 4 7 4 6 8 4 ·
9 9 | 7 7 8 4
9 | 7 8
| 8
在每个三角形中,我们都对这些树中的 forest 感兴趣,它们最低限度地确定了我们实际关心的元素:
|---------------------------------|
1 7 0 4 0 9|0 7 0 4 5 7 4 3 4 6 3
7 4 9 | 7 4 7 4 6
9 | 7 7
| 7
请注意,在这种情况下,左侧有两棵树,右侧有三棵树。树的总数最多为O(log k),因为任何给定高度最多有两棵树。我们可以通过一点点数学找到分裂点
round_to = (start ^ end).bit_length() - 1
split_point = (end >> height) << height
请注意,Python 的 bit_length 可以通过 x86 架构上的 lzcnt 指令快速完成。相关树位于拆分的每一侧。相关子树的大小被编码在这些数字的残差位中:
lhs_residuals = split_point - start
rhs_residuals = end - split_point
bin(lhs_residuals)
# eg. 10010110
# sizes = 10000000
# 10000
# 100
# 10
很难遍历整数的最高有效位,但是如果您进行位交换(字节交换指令加上一些移位和掩码),则可以通过迭代来遍历最低有效位:
new_value = value & (value - 1)
lowest_set_bit = value ^ new_value
value = new_value
遍历左半部分和右半部分需要 O(log k) 预期时间,因为最多有 2log₂ k 树 - 每边一个位。
切线:处理O(1) 时间和O(n log n) 空间中的残差
O(log k) 比O(log n) 好,但仍然不是开创性的。先前尝试的一个有益效果是,每一侧的树都“连接”到一侧。他们的切片中只有n 范围,而不是任意切片的n²。您可以通过添加每个级别的累积最大值来利用它,如下所示:
1 8 4 5 4 0 1 5 6 9 1 7 0 4 0 9 0 7 0 4 5 7 4 3 4 6 3 8 2 4 · ·
- 8|- 5|- 4|- 5|- 9|- 7|- 4|- 9|- 7|- 4|- 7|- 4|- 6|- 8|- 4|- · left to right
8 -|5 -|4 -|5 -|9 -|7 -|4 -|9 -|7 -|4 -|7 -|4 -|6 -|8 -|4 -|· - right to left
- - 8 8|- - 4 5|- - 9 9|- - 4 9|- - 7 7|- - 7 7|- - 6 8|- - · · left to right
8 8 - -|5 5 - -|9 9 - -|9 9 - -|7 7 - -|7 7 - -|8 8 - -|4 4 - - right to left
- - - - 8 8 8 8|- - - - 9 9 9 9|- - - - 7 7 7 7|- - - - 8 8 · · left to right
8 8 5 5 - - - -|9 9 9 9 - - - -|7 7 7 7 - - - -|8 8 8 8 - - - - right to left
- - - - - - - - 8 9 9 9 9 9 9 9|- - - - - - - - 7 7 7 8 8 8 · · left to right
9 9 9 9 9 9 9 9 - - - - - - - -|8 8 8 8 8 8 8 8 - - - - - - - - right to left
- - - - - - - - - - - - - - - - 9 9 9 9 9 9 9 9 9 9 9 9 9 9 · · left to right
9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 - - - - - - - - - - - - - - - - right to left
标记- 用于忽略那些必须与它们下面的级别相同的部分,不需要复制。在这种情况下,相关切片是
|---------------------------------|
1 7 0 4 0 9 0 7 0 4 5 7 4 3 4 6 3
↓ ↓
9 9 9 9 - - - -|- - - - - - - - 7 7 7 8 8 8 · ·
right to left | left to right
和所需的最大值如所示。真正的最大值是这两个值中的最大值。
这显然占用了O(n log n) 内存,因为有log n 级别并且每个级别都需要一整行值(尽管它们可以是索引以节省空间)。然而,更新需要O(n) 时间,因为它们可能会传播 - 例如,向其添加 10 将使整个底部从右到左的行无效。突变显然同样低效。
O(1) 时间通过回答不同的问题
根据您需要的上下文,您可能会发现可以截断搜索深度。如果允许切片中相对于切片大小有一些余地,则此方法有效。由于切片在几何上收缩,尽管来自0:4294967295 的切片需要大量的 22 次迭代,但截断到固定数量的 11 次迭代会得到切片 0:4292870144 的最大值,相差 0.05%。这可能是可以接受的。
O(1) 预计利用概率的时间
舍入可能是可以接受的,但即使是这样,您仍在使用O(log n) 算法 - 只是使用较小的固定n。在随机分布的数据上可以做得更好。
考虑森林的一侧。当你遍历它时,你看到的数字的分数超过了你没有看到的几何分数。因此,您已经看到标准杆最大增加的概率。你可以利用它来发挥你的优势是有道理的。
再考虑这一半:
---------------------|
0 7 0 4 5 7 4 3 4 6 3 8 2 4 · ·
7 4 7 4 6* 8 4 ·
7 7 8* 4
7* 8
8
检查7*后,不要立即遍历6*。相反,检查其余的 all 中最小的父级,即8*。仅当此父级大于迄今为止的最大值时才向下遍历。如果不是,您可以停止迭代。只有更大了才需要继续往下遍历。碰巧这里的最大值在末尾之后,所以我们一直向下遍历,但你可以想象这是不寻常的。
至少一半的时间你只需要评估第一个三角形,至少一半的时间你只需要向下看一次,等等。这是一个几何序列,显示平均遍历成本是 两次遍历;如果你包括剩余的三角形在某些时候可能小于一半大小的事实,则更少。
在最坏的情况下呢?
最坏的情况发生在非随机树上。最病态的是排序后的数据:
---------------------|
0 1 2 3 4 5 6 7 8 9 a b c d e f
1 3 5 7 9 b d f
3 7 b f
7 f
f
由于最大值始终位于您未见过的范围的片段中,无论您选择哪个切片。因此遍历总是O(log n)。不幸的是,排序数据在实践中很常见,这个算法在这里受到了伤害(这个属性与其他几个算法共享,比如快速排序)。不过,可以减轻伤害。
不会死于排序数据
如果每个节点都说明它是排序的还是反向排序的,那么在到达该节点时,您无需再进行任何遍历 - 您只需获取子数组中的第一个或最后一个元素。
---------------------|
0 1 2 3 4 5 6 7 8 9 a b c d e f
→ → → → → → → →
→ → → →
→ →
→
您可能会发现您的数据大部分是排序的,但有一些小的随机化,这会破坏该方案:
---------------------|
0 1 2 3 4 5 6 7 a 9 a b d 0 e f
→ → → → ← → ← →
→ → b f
→ f
f
因此,每个节点都可以在保持排序的同时向下移动的最大级别数,以及在哪个方向上。然后你跳过那么多迭代。一个例子:
---------------------|
0 1 2 3 4 5 6 7 a 9 a b d 0 e f
→1 →1 →1 →1 ←1 →1 ←1 →1
0 3 5 7 a b d f
→2 →2 →1 →1
3 7 b f
→3 →2
7 f
→3
f
→n 表示如果您跳过n 级别,则节点将全部从左到右排序。顶部节点是→3,因为向下排序了三层:0 3 5 7 a b d f。方向很容易用一位编码。因此,大部分排序都得到了优雅的处理。
这很容易保持更新,因为每个节点都可以从其直接子节点计算其值。如果他们同意并且按照他们同意的相同方向排序,则最小距离并加一。否则重置为1 的距离并指向子元素的排序方向。最难的是遍历的逻辑,看起来有点挑剔。
仍然有可能产生需要一直遍历到底部的示例,但它们不应在非对抗性数据中频繁出现。