Arrays 用于在优于O(logn)的预期时间内查找子阵列最大值的数据结构

Arrays 用于在优于O(logn)的预期时间内查找子阵列最大值的数据结构,arrays,data-structures,language-agnostic,time-complexity,Arrays,Data Structures,Language Agnostic,Time Complexity,给定一个值数组,如何构建一个数据结构,让您快速找到任何连续子数组的最大值?理想情况下,建造这种结构的开销应该很小,结构应该允许有效的附加和单个元素的变异 示例数组是[6,2,3,7,4,5,1,0,3]。请求可能是从索引2到7(子数组[3,7,5,1,0])查找切片的最大值,这将导致7让n为数组的长度,k为切片的长度 天真的,O(logk),方法 一个显而易见的解决方案是构建一棵树,反复给出最大值的成对摘要 1 8 4 5 4 0 1 5 6 9 1 7 0 4 0 9 0 7 0 4 5 7

给定一个值数组,如何构建一个数据结构,让您快速找到任何连续子数组的最大值?理想情况下,建造这种结构的开销应该很小,结构应该允许有效的附加和单个元素的变异


示例数组是
[6,2,3,7,4,5,1,0,3]
。请求可能是从索引2到7(子数组
[3,7,5,1,0]
)查找切片的最大值,这将导致
7

n
为数组的长度,
k
为切片的长度

天真的,
O(logk)
,方法 一个显而易见的解决方案是构建一棵树,反复给出最大值的成对摘要

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(logn)
时间。如果需要,还有许多其他领域需要优化

所选切片可以拆分为两个切片,在两个三角形之间的边界上拆分。在本例中,对于给定的切片,我们将拆分为:

                   |---------------------------------|
                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
在每个三角形中,我们感兴趣的是这些树中的一部分,它们最小限度地决定了我们真正关心的元素:

                   |---------------------------------|
                    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
很难遍历整数的最高有效位,但如果您进行位交换(一条byteswap指令加上几个移位和掩码),则可以通过迭代来遍历最低有效位:

new_value = value & (value - 1)
lowest_set_bit = value ^ new_value
value = new_value
沿左半部和右半部向下遍历需要
O(logk)
预期时间,因为最多有
2log₂ k
树-每边一位

切线:在
O(1)
时间和
O(n logn)
空间中处理残差
O(logk)
O(logn)
好,但它仍然不是突破性的。上一次尝试的一个有益效果是,两边的树都“附着”在一边;在它们的切片中只有
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 logn)
内存,因为有
logn
级别,每个级别都需要一行完整的值(尽管它们可以作为索引以节省空间)。但是,更新需要花费
O(n)
时间,因为它们可能会传播-例如,向该行添加10将使整个从右下到左的行无效。突变显然同样低效

O(1)
通过回答不同的问题来计时 根据所需的上下文,您可能会发现可以截断搜索深度。如果您的切片相对于切片的大小有一定的回旋余地,那么这是可行的。由于切片在几何上收缩,尽管
0:4294967295
的切片需要大量的22次迭代,但截断到11次迭代的固定数量会使切片
0:4292870144
的最大值相差0.05%。这是可以接受的

O(1)
利用概率的预期时间 舍入可能是可以接受的,但即使是这样,您仍在执行
O(logn)
算法-只需使用较小的、固定的
n
。在随机分布的数据上可以做得更好

想想森林的一边。当你向下移动时,你看到的数字的分数超过了你几何上没有看到的分数。因此,你已经看到最大的概率在PAR中增加。这是有道理的,你可以利用这对你的优势

再考虑一下这一半:

---------------------|
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*
。取而代之的是,检查所有其余项的最小父项,即
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(logn)
。不幸的是,排序数据在实践中很常见,并且该算法在这里受到了损害(该属性与其他一些算法共享,如quicksort)。不过,减轻伤害是可能的

不因排序数据而死亡 如果每个节点都说它是排序的,还是反向排序的,那么在到达该节点时,您不需要再进行任何遍历-您只需获取子数组中的第一个或最后一个元素

---------------------|
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
因此,每个节点可以有t
---------------------|
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