什么是用于保持累积值的良好数据结构?

11

我正在寻找一种非常高效的想法、概念或数据结构,用于访问保持累积值的集合。

举个例子来说明我的需求:

我有一个值列表(2,3,5)。这个列表在观察累加值时是(2,5,10)。

现在我在列表开头添加1并得到(1,2,3,5),在累积方面是(1,3,6,11)。

我只需要查看累积值,对1、2、3和5不感兴趣。我需要能够快速插入位置、删除位置,并且所有这些操作都可以快速更新累积数组(最好不需要遍历整个数组并重新计算值)。

有什么想法或提示吗?

@Kristo (评论中写不下): 为了说明负数会使总和值失去意义,请看以下示例。

插入1,然后是-1。和是1,然后是0。(1,-1)//(1,0) 插入3,接着插入-3。和是3,然后是0。(1,3,-1,-3)//(1,4,3,0) 插入2,接着插入-2。和是2,然后是0。(1,3,2,-1,-2,-3)//(1,4,6,5,3,0)

如果我的"魔数"是4,总和值将不能告诉我是否超过了它。

PS: 主要原因是为了能够判断我是否超过了某个值以及在链中的哪个位置。


你会从列表中弹出随机元素吗?你将沿着哪个方向遍历它? - Aiden Bell
在数组中插入或删除元素时,不应该需要对整个数组进行计算。计算只需要从插入/删除点开始即可。 - shahkalpesh
另外一个重要的因素是你预期的工作量。你是优化读取速度,插入速度还是让时间相等? - Matt
@shahkalpesh 我知道这一点,但仍然需要大量重新计算。我想经常添加或删除值。 - Tomas Pajonk
2
请定义对你而言何为“好”。如前所述,使用模式是很重要的。你是在优化空间、时间还是实现的易用性?请给我们你评判“好”的标准。 - dss539
显示剩余2条评论
8个回答

5

5
我能想到的唯一优化方法是对累积列表进行“懒惰”评估。除了您的源值列表外,还要跟踪累积列表中准确的最高位置的编号。如果您需要一个更高的数字,则向上遍历列表,更新值和索引。当然,如果您通常在列表早期添加项目,那么这并没有太大的好处...
idx  values       cumulative    operation
 3   (2,3,5)      (2, 5, 10)
 0   (1,2,3,5)    (X,X,X,X)     在0处插入1 
 3   (1,2,3,5)    (1,3,6,X)     查找超过5的值
 3   (1,2,3,5,4)  (1,3,6,X,X)   在4处插入4

这看起来非常合理。 - Tomas Pajonk

5

使用具有额外属性的二叉搜索树,即节点包含其子树之和。所有操作仍然是O(lg n)。要插入或删除值,您需要执行正常的过程,并更新所有父项的总和。获取总和就像找到包含元素的节点并返回其总和减去其右子节点的总和一样简单。


3
在C#中,我会将所有实际值存储在列表中,并使用自定义迭代器循环遍历累积值。
只有当迭代器告诉您超过限制时(显然,您必须编写代码),您才会重新计算。
我认为价值在于,在迭代列表之前可以添加/删除,而无需进行任何计算(我认为您需要这样做才能找到截止数字)。

1
  • 您可以查看一个二叉索引树的累计频率数据结构
  • 您可以将值范围分成固定的位范围。例如,3个间隔:

    #define NUM (1<<24)  // 数据集中的最大值
    #define BITS0 8
    #define BITS1 8
    int cum0[NUM >> (BITS0+BITS1)]; // cum1 的总和
    int cum1[NUM >> BITS1]; // 计数的总和
    int count[NUM];
    
    int add(id, val) { // 添加一个值
      cum0[id >> (BITS0+BITS1)] += val;
      cum1[id >> BITS1] += val; 
      count[id] += val;                     
    }
    
    int cumvalue(int id) { int cum = 0; // 返回索引id处的cum值         
      for(i = 0; i < (id >> (BITS0+BITS1));i++) cum += cum0[i]; i <<= BITS0;
      for(i = (id & ~((1 << (BITS0+BITS1))-1)) >> BITS1; i < (id >> BITS1); i++) cum+= cum1[i]; i <<= BITS1;
      for(i = id & ~((1 << BITS1) -1); i < id; i++) cum += count[i];            
      return cum;
    }
    

1

我看到有两种简单的方法,都使用基本数据类型 - 列表。

  1. 保留原始列表,在每次更改时重新计算累积值。

  2. 仅保留累积列表,并使用以下函数进行添加或删除:

    • Add(item,position 默认为列表末尾) 将从位置-1开始添加项目的值。
    • Delete(position) 将计算两个数字的原始值,然后将此数字从列表的其余部分减去,然后再删除该项。

    Add 2 : (2) 将2添加到空列表中。

    Add 3 : (2,5) 将3添加到列表末尾,成为前一个元素(2)的后继元素。

    Add 5 : (2,5,10) 将5添加到列表末尾,成为前一个元素(5)的后继元素。

    Add 1 at start: (1,3,6,11) 在列表开头添加1,并逐个增加1直到结尾(没有前置元素)。

    Add 7 at 2nd position: (1,8,11,14,19) 添加7并逐个增加7直到结尾(没有前置元素)。

    Delete 3rd position (The 11) : (1,8,3,8) 获取该值,删除它,将该值添加到其余部分。

这种方式可以保持所有内容的同步,而无需保留原始值。


谢谢您的回答。这仍然基本上优化了内存空间,但在更改后仍然需要重新计算整个列表。我想知道是否可以部分避免这种情况。 - Tomas Pajonk
我认为你在末尾犯了几个错误。在位置2处添加7应该产生一个累积列表 (1, 8, 10, 13, 18),实际的列表将是 (1, 7, 2, 3, 5)。所以然后移除位置3应该产生 (1, 8, 11, 16),从实际的列表 (1, 7, 3, 5)。 - SergioL
我认为你不能在不加它们的情况下添加数字。 你期望列表有多大??这会在微控制器上吗?加法已修复。 - Osama Al-Maadeed
你还是有点偏离,我认为。此外,删除第三个位置不应被解释为将负数添加到列表中...你的累积列表应该在这里增长。 - SergioL

1

用C++的术语来说,你能用std::list(在中间轻松插入/删除)或者std::set(始终排序)来存储数据,并使用一个变量来保存总和吗?每次插入/删除时,根据需要修改总和。总和代表了你将要累积列表中的最大数。只有当你超过了魔法数字时,才需要进行一些算法工作来找出你超过的位置。

更新:

根据你提供的新信息,我没有看到很多捷径可用。由于你需要经常在中间插入或删除,所以这暗示了某种链表的方法。你可以通过仅更新已更改的部分来节省一点计算量。假设L是值的列表,n是列表中的目标位置。要在位置n插入值x

  • 在位置n插入值x + L(n-1)
  • x添加到这个新n之后的所有元素
  • 如果超过了魔法数字,则停止

对于删除操作,过程是相同的,只不过你需要从所有后续值中减去相应的数值。这样,只有在插入到开头附近时才需要进行大量的工作。


我能看到的唯一问题是当负值出现时。 - Tomas Pajonk
修改了问题,以展示负数使总值变得不太有用的示例。 - Tomas Pajonk
Kristo,我认为跟踪这个的算法最终会等同于每次重新计算(部分)列表。 - dss539

0

使用树状数组

这将使期望的运行时间复杂度随元素数量对数增长,而不是像朴素实现一样线性增长。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接