【问题标题】:Running sum of the last n integers in an array数组中最后 n 个整数的运行总和
【发布时间】:2014-07-29 20:06:28
【问题描述】:

假设一个进程每 60 秒接收一个新整数。我想保留最后 5 个数字的总和。例如:

3 1 99 10 8 0 7 9 --> running total is 10+8+0+7+9==34
       <--------->

60 秒后,我们收到一个新整数。接收到的整数列表现在如下所示:

3 1 99 10 8 0 7 9 2 --> running total is now 8+0+7+9+2==26
          <-------->

如果你有存储空间来保存最后 5 个整数,这很容易实现。我正在尝试提出一种内存效率更高的算法。有人有什么想法吗?

【问题讨论】:

  • 你的内存太痛苦了,20字节太多了(假设sizeof(int) == 4)?
  • 这似乎更像是一个编程难题,而不是一个实际的编程问题。也许您应该将其发布在 codegolf.stackexchange.com。
  • 我可能错了,但我认为你不能那样做。如果您可以将任意数据的最后 n 个数字的总和存储在小于 O(n) 的内存中,这意味着您可以将 n 个任意数字存储在小于 O(n) 的内存中,这是荒谬的。
  • 为了便于讨论,我正在简化问题。在实践中,大约会有 8000 个这样的列表,我需要保留最后 5、60 和 3600 个元素的运行总和。这就是为什么我如此关注内存高效解决方案的原因。
  • 如果尺寸对你来说太大了,我会开始寻找替代解决方案。你真的需要精确的总和吗?每个数字你真的需要多少位?可变长度编码有帮助吗?存储增量有帮助吗?你可以使用一些实际的压缩算法,它会节省空间还是足够快?等等。

标签: c algorithm


【解决方案1】:

由于您可以重建最后 n 个数字,例如,如果您输入 n 个零,那么您所做的任何事情都相当于存储最后 n 个数字。

假设数字可以是真正随机的并且每个数字都是 b 位长,因此任何正确的算法都可以精确地再现 nb 个随机位。这需要至少 nb 位的存储空间。

【讨论】:

  • 我上面的评论有相同的结论。我无法正式证明这一点,但在我看来你是对的。
  • +1 用于推理问题。潜入并编写代码总是很诱人,但代码会分散思考。
【解决方案2】:

我认为您无法按照描述解决此问题。

对于最近两个整数的运行总和,您必须至少存储第一个整数和当前运行总和,以重建第二个(或最后一个)整数。这意味着存储两个整数。

给定第一个整数:

一个1

可以迭代计算最后两个索引ij的运行总和si,j随着整数 a2 等进入流,重用之前的运行总和:

s1,2 = a1 + a2

s2,3 = s1,2 - a1 + a3

s3,4 = s2,3 - (s1,2 - a1) + a4

s4,5 = s3,4 - (s2,3 - (s1,2 sub> - a1)) + a5

...

等等,以递归方式。

如您所见,两个整数的运行总和至少需要 a1 和运行总和 si-2,i- 1,重构倒数第二个元素。

同样,对于最近三个整数的运行总和,您必须至少存储前两个整数和当前运行总和,以重构第三个(或倒数第二个)整数。

给定第一个和第二个整数:

一个1,一个2

最后三个索引ij和si,j,k em>k 可以迭代计算,因为整数 a3 等进入流,重用之前的运行总和:

s1,2,3 = a1 + a2 + a3

s2,3,4 = s1,2,3 - a1 + a4

s3,4,5 = s2,3,4 - a2 + a5

s4,5,6 = s3,4,5 - (s1,2,3 - a 1 - a2) + a5

...

同样,您必须为运行总和存储尽可能多的整数,以重建丢失的整数。通过归纳,如果您消除任何一个变量,您将无法概括缺失值。

【讨论】:

  • 另一种思考方式是,你有一个函数 f 将一些整数集 A 映射到另一个集 B (f:A->B)。假设这里的目标是对 B 中的整数集合求和,生成运行和,集合 A 是那些整数 a1a2 等我们想要存储在我们的数组中。如果集合 A 的基数小于集合 B 的基数,则 A 中的至少一个数可以映射到两个或B 中有更多数字。您如何以可重复的方式选择任一数字,在计算机程序中?
【解决方案3】:

为了讨论,我正在简化问题。在 练习,会有 8000 个左右这样的列表,我需要保留 最后 5、60 和 3600 个元素的运行总和。

听起来您想要过去 5 秒、60 秒和 1 小时的总数。

您真的需要 60 秒的总时间才能精确到秒吗?还是可以每 5 秒更新一次?同样,您是否需要将每小时总数精确到秒,还是每分钟更新一次就可以了?

如果您不需要将每分钟和每小时的总计精确到秒,那么您可以节省大量存储空间。在这种情况下,5 + 12 + 60 = 77,而不是 3600

然后算法运行如下:

//these are the running totals that will be displayed
int last1 = 0;    //updated every second
int last5 = 0;    //updated every second
int last60 = 0;   //updated every 5 seconds
int last3600 = 0; //updated every minute

// 3 circular buffers:
// last 5 1-second periods (updated every second)
int period1[5] = {0};
// last 12 5-second periods (updated every 5 seconds)
int period5[12] = {0};
// last 60 1-minute periods (updated every minute)
int period60[60] = {0};

//indexes for the circular buffers
int index1 = 0;
int index5 = 0;
int index60 = 0;
while (1) {
    printf("1s 5s 1m 1h\n");
    printf("%2d %2d %2d %2d\n", last1, last5, last60, last3600);

    sleep(1);
    last1 = getNewValue();

    //update last5 by subtracting the expiring period and adding the new one
    last5 -= period1[index1];
    last5 += last1;
    //and save the new period to circular buffer
    period1[index1] = last1;
    index1++;

    //if we get to the end of the circular buffer we must go to the start
    //we have also completed a 5s period so we can update last60
    if (index1 >= 5) {
        index1 = 0;

        //similar to before
        last60 -= period5[index5];
        last60 += last5;

        period5[index5] = last5;
        index5++

        //similar to above, but now we have completed a 60s period
        //so we can update last3600
        if (index5 >= 12) {
            index5 = 0;

            //similar to before
            last3600 -= period60[index60];
            last3600 += last60;

            period60[index60] = last60;
            index60++

            if (index60 >= 60) {
                index60 = 0;
            }
        }
    }
}

如您所见,总共只需要 84 个整数,并且没有进行循环,因此性能会很好。

如果您希望每秒而不是每 5 秒更新 60 秒的总数,您可以这样做。您还可以变得更加繁琐,例如1 小时周期每 20 秒更新一次。但是,代码如此简洁的部分原因是,每次完成低于它的一个周期时,每个周期都会更新。

请注意,总共 3600 秒是使用最多内存的时间,因此您需要格外小心。

【讨论】:

    【解决方案4】:

    我不相信你能做到。您需要一个能够保存最后一个 n 值的滑动窗口。

    您可以做的最好的事情是使用模数运算将数组视为循环缓冲区,在执行过程中保持运行总和和计数,以避免必须遍历整个缓冲区来计算值的总和。像这样:

    #include <stdlib.h>
    #include <string.h>
    #include <stdio.h>
    
    #define WINDOW_SIZE 5
    
    static int   *window      ;
    static int    i           ;
    static double sum         ;
    static double cnt         ;
    
    double record_value( int value )
    {
      double mean ;
    
      i          = (i+1) % WINDOW_SIZE ;
      sum        = sum - window[i] + value ;
      cnt       += cnt < WINDOW_SIZE ? 1 : 0 ;
      window[i]  = value ;
    
      mean = sum/cnt ;
      return mean ;
    }
    
    void log_message( double avg )
    {
      int x = 0 ;
    
      printf( "%f = ( " , avg ) ;
      for ( int x = 0 ; x < cnt ; ++x )
      {
        printf( "%s%d" , x > 0 ? " + " : "" , window[x] ) ;
      }
      printf( " ) / %d\r\n" , (int)cnt ) ;
      return ;
    }
    
    int main( int argc, char* argv[] )
    {
      int j ;
    
      window      = calloc( WINDOW_SIZE , sizeof(window[0]) ) ;
      i           = WINDOW_SIZE - 1 ;
      sum         = 0 ;
      cnt         = 0 ;
    
      for ( j = 0 ; j < 100 ; ++j )
      {
        int    v   = rand() ;
        double avg = record_value( v ) ;
    
        log_message( avg ) ;
    
      }
    
      return 0 ;
    }
    

    【讨论】:

    • 循环缓冲区是个好主意。它不仅高效,而且您不需要单独的列表来计算多个长度的总和 - 只需从缓冲区中的不同位置进行减法即可。
    【解决方案5】:

    如果您的输入有一些限制,也许有一些 hacky 方法可以解决这个问题。

    char 占用 1 个字节。给定您的输入示例,如果您的整数是正值且长度小于三位数,介于 0 和 99 之间,那么您可以通过将整数减少到 char 流拆分来节省一些空间分隔符。

    给定如下数字流的尾随总和:

    3 1 99 10 8 0 7 9
    

    也许这可以简化为存储两个元素:最后五个元素作为不断realloc-ed char * 和总和作为int

    "10|8|0|7|9" (10 bytes)
    34 (4 bytes)
    

    这需要总共 14 个字节,比存储五个 int 值所需的 20 个字节少 6 个字节。

    您需要编写代码来标记并从char * 中提取元素以重新计算总和,并且您需要realloc 并在新元素进入并且缓冲区长度发生变化时重写字符缓冲区,以便您总是在最大限度地节省潜在的空间。

    还要注意 char * 上缺少 NULL 终止符 - 您不希望将其视为字符串,以最大限度地提高存储效率。 NULL 是浪费的字节。

    您还需要一种谨慎的方式来重写char *,这样您就不必在中间存储上浪费空间。对于一个非常大的char *,您可能会在一个四字节的size_t 上浪费空间来记录流真正开始的偏移量,这样您就不会浪费时间重写它,而一个四字节的@987654335 @value 以便您知道何时结束并需要回绕(或者您会在 NULL 上浪费一个字节,并对此进行测试)。

    一个由五个一位或两位整数组成的流,带有四个分隔符且没有 NULL,最多需要 16 个字节,最少需要 9 个字节。存储为int 的累积总和将占用 4 个字节。最坏的情况是,您使用与五个int 变量相同的存储空间。在最好的情况下,您使用 13 个字节——比最坏的情况少 7 个字节。

    假设并非所有整数都是两位数,您可能会节省一些空间。但是,给定一个从 0 到 99 的均匀随机整数流,您会期望 90% 的这些随机数是两位数。因此,平均而言,这可能会在大多数情况下使用接近 20 个字节。

    如果您真的想成为一个小气鬼,请将累积和存储为三个字节的char *。最大和(给定相同的约束)将为 99 + 99 + 99 + 99 + 99 = 495。值 "495" 可以存储在三个字节中。所以这是一个额外的节省。

    请注意,这并没有考虑操作系统的字长和其他可能填充数据结构的优化等。因此,这个非常简单且受限制的示例最终甚至可能无法真正节省尽可能多的空间。

    如果您要处理非常大的流,请考虑使用类似的方法,该方法将使用块级压缩算法,例如 bzip2 或 gzip。根据数据的规模,您节省的存储空间可能比压缩开销损失的多。您可能希望避免需要提取整个流以仅恢复第一个整数的编码方案。

    【讨论】:

      【解决方案6】:

      如果您必须不断迭代新值,我认为存储的变量不会少于 5 个。如果您的所有整数都很小,则将所有 5 个值存储在更合适的类型(char)中可能是有意义的,这将比 int 使用更少的空间。

      【讨论】:

        【解决方案7】:

        让我们做一个 cs 风格的缩减。

        我将假设您的问题是可能的,并表明我们可以创建一个无损压缩算法,它的输出总是比输入短。

        压缩算法(压缩为 5 字节块):
        将 5 个字节相加,存储为新的 11 位整数。我猜我们可以使用 2 个整数字节。它仍然是压缩。

        解压算法(取2字节,返回5字节):
        调用我们的运行总数,即 2 字节的数字。

        将 0 添加到“列表”(引用是因为没有列表。我们只有我们的运行总数)。将新的运行总数与旧的比较。不同的是第一个字节。

        在列表中添加另一个 0。再比较一下。

        再重复 3 次。你有你的 5 个字节回来。

        从这里我们看到我们肯定需要额外的内存。因为我们知道这样的压缩算法是不可能的。

        【讨论】:

          猜你喜欢
          • 2023-02-26
          • 2018-08-09
          • 2021-02-12
          • 2012-08-26
          • 1970-01-01
          • 2019-04-02
          • 1970-01-01
          • 1970-01-01
          • 2021-10-21
          相关资源
          最近更新 更多