对于 24 小时逐分钟的布尔记录,什么是好的数据结构

What's a good data structure for a 24hr minute-by-minute boolean record

提问人:sbi 提问时间:3/24/2014 最后编辑:sbi 更新时间:10/27/2014 访问量:863

问:

我的任务是创建一个数据结构,该结构在过去 24 小时内的每一分钟都保存一个布尔值。(是否发生了事件 X?我需要一直保留最后 24 小时。(也就是说,数据会不断添加,旧数据会弹出。数据将保存到闪存驱动器中。我们使用的是嵌入式平台,但内存并不那么有限(我有 128MB 可用内存),不过碎片可能会成为一个问题。这是一个实时系统,但由于记录是每分钟的,因此几乎没有运行时限制。

界面可能如下所示:

class x_record {
  public:
    // record whether or not x occurred this minute
    void record_entry(bool x_occured);

    // how many minutes has x occured in the last 24hrs? 
    unsigned int x_occurance_minutes() const;

  private:
    // HERE BE DRAGONS 
};

存储实际数据的好数据结构是什么?我目前最喜欢的是 和 24 的数组,其中 60 位中的 64 位每个 60 位用于 60 分钟的小时。后者是目前最爱的坚持。std::deque<bool>long long

我想我对这两种想法的利弊都有一个很好的了解,但希望你们中的一些人能提供额外的内部信息,甚至更多的想法。

P.S.:这是严格的 C++03 + TR1 + Boost 1.52,没有可用的 C++11/14。

C++ 数据结构

评论

5赞 Engineer2021 3/24/2014
听起来你需要一个环形缓冲区。
2赞 sbi 3/24/2014
@Frédéric 它应该是可维护的、快速的、紧凑的、美观的和行为良好的。如果它冲泡出好咖啡,那么这是一个不错的额外功能。:)
2赞 Sergey Kalinichenko 3/24/2014
@sbi 我的理解是,你不需要其中任何一个:你分配一个 ,然后用它运行。在内部,它将具有与多头数组相同的占用空间,外加两个指针。它碎片化堆的可能性也非常小,因为所有分配都将以相同的 180 字节大小进行。vector<bool>(24*60, false)
2赞 Marius Bancila 3/24/2014
另一种方法是使用 .std::bitset
8赞 sbi 3/24/2014
@Alec:请不要以这种有争议的方式更改我的代码。我不认为受保护的数据是一个好的设计。事实上,我认为有时是必要的邪恶(如),但应该尽可能少地使用。protectedfriend

答:

3赞 ypnos 3/24/2014 #1

我每小时都会有一个,而且每小时只放弃一次。所以你可以有一个.同样,它可能是一个,但与向量相比,我没有看到任何好处。std::vector<bool>std::deque<std::vector<bool> >std::deque<long long>

它使事情变得高效、易于理解且不易出错。

评论

0赞 sbi 3/24/2014
这是两个截然不同的想法,只是以骨架的形式呈现,利弊根本没有得到解决。对不起,但这是我写的。-1
0赞 Alec Teal 3/24/2014
为什么我们不能静态分配(见我的答案)
0赞 ypnos 3/24/2014
@sbi 我在哪里提出两个截然不同的想法?我在我的答案中只看到一个想法,那就是每小时有一个出列项目。这基本上是你的两个建议之间的折衷。
0赞 sbi 3/25/2014
@ypnos:A 允许将布尔值作为(如果它们是)实际对象进行访问,而(注意容器的拼写,顺便说一句)需要我自己摆弄这些位。基本上,这归结为我在问题中提出的两个相反的建议。vector<bool>boolstd::deque<long long>
0赞 ypnos 3/25/2014
也许你应该更仔细地表达你的问题,我也认为你没有得到我答案的精髓,长长只是一个旁注。
9赞 Paweł Stawarz 3/24/2014 #2

为了详细说明版本,我认为这是一个不错的主意,因为您始终存储相同数量的数据(至少我是这样理解的):vector<bool>

class x_record {
   vector<bool> data;
   unsigned int currentMinute;

public:
   x_record(): data(24*60, false), currentMinute(0){}

   // record whether or not x occurred this minute
   void record_entry(bool x_occured){
      data[currentMinute] = x_occured;
      currentMinute = (currentMinute+1)%data.size();
   }

};

这样,向量大小是恒定的,因此它不应该被分割(因为它是同时分配的)。您可以使用变量跟踪当前分钟。当您填写所有字段时,只需将其设置为并覆盖旧数据,因为您不需要它。currentMinute0%(24*60)

您也可以使用普通数组而不是 ,但这需要更多空间(因为通常 C++ 存储值的方式与 ),或者一些位操作 - 在我看来 - 当我们获得专业化时重新发明轮子。vector<bool>boolcharvector<bool>

评论

0赞 sbi 3/24/2014
我不认为这是一个坏主意。 从我这里。+1
0赞 Frerich Raabe 3/24/2014
+1 我会给出同样的答案。不过,我不会硬编码,而是使用 .此外,OP 请求的方法只是24*60record_entrydata.size()unsigned int x_occurance_minutes() const;return std::count( data.begin(), data.end(), true );
0赞 ypnos 3/24/2014
这只是一个普通的环形缓冲区,查找事件有点棘手,因为您必须弄清楚它在环形缓冲区中的位置,首先是向量元素,然后是有问题的位。
1赞 Frerich Raabe 3/24/2014
您可以立即使用初始值设定项列表中构造具有正确大小的成员。datadata(24*60, false)
1赞 king_nak 3/24/2014 #3

当您只关心事件发生在过去 24 小时内的频率,而完全忽略事件发生的时间时,您可以简单地记录事件发生的时间。

请考虑以下情况(未测试):

class x_record {
public:
  // record whether or not x occurred this minute
  void record_entry(bool x_occured) {
    if (x_occured) {
      m_records.insert(getTime());
    }
  }

  // how many minutes has x occured in the last 24hrs? 
  unsigned int x_occurance_minutes() {
    clearRecords();
    return m_records.size();
  }

private:
    time_t getTime() const {
      return time(NULL) / 60; // Get Minute time stamp
    }

    void clearRecords() {
      // Erase all records that happend before the last 24 hours
      time_t frameStart = getTime() - 60*60*24;
      for (std::set<time_t>::iterator it = m_recods.begin(); it != m_records.end(); ++it) {
        if (*it < frameStart) {
          m_records.erase(it);
        } else {
          break;
        }
      }
    }

private:
    std::set<time_t> m_records;
};

如果事件很少发生,则这是最合适的。

它使用集合以严格的弱排序存储其元素的约束,因此时间戳较低的元素将首先列出。也

您应该考虑使用不同的键类型插入到集合中,因为不能保证表示秒数。time_t

评论

0赞 sbi 3/24/2014
这是一个有趣的想法()。但是该事件可能在 24 小时内发生长达几十分钟,发生几十次的可能性不大,并且该平台上的时间戳为 64 位。鉴于这些情况(我相信我在问题中没有明确说明),我想使用你的想法会很差。+1
6赞 bames53 3/24/2014 #4

循环缓冲区:

int countBits(std::uint32_t v) {
  // source: http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel
  typedef std::uint32_t T;
  v = v - ((v >> 1) & (T)~(T)0/3);                           // temp
  v = (v & (T)~(T)0/15*3) + ((v >> 2) & (T)~(T)0/15*3);      // temp
  v = (v + (v >> 4)) & (T)~(T)0/255*15;                      // temp
  return (T)(v * ((T)~(T)0/255)) >> (sizeof(T) - 1) * CHAR_BIT; // count
}

class x_record {
  public:
    x_record() { std::memset(&history, 0, sizeof(history)); }

    // record whether or not x occurred this minute
    void record_entry(bool x_occured) {
      uint64_t mask = 1 << (bit % 32);
      uint64_t set = x_occured ? mask : 0;
      history[bit / 32] = (history[bit / 32] & ~mask) | set;

      bit = (bit + 1) % (24*60);
    }

    // how many minutes has x occured in the last 24hrs? 
    int x_occurance_minutes() const {
      int count = 0;
      for (int i=0; i<45; ++i) {
        count += countBits(history[i]);
      }
      return count;
    }

  private:
    std::uint32_t history[45];
    short bit = 0;
};

评论

0赞 sbi 3/25/2014
这就是我认为我会用的一点黑客。使用 32 位整数比使用 64 位整数有什么优势吗?(哦,还有,因为这也回答了这个问题。long long+1
0赞 bames53 3/25/2014
@sbi 24 * 60 可以被 32 整除,因此不会使用额外的空间。您也可以使用,但我发现此位计数例程仅适用于最多 32 位的类型。您必须更新 这两个常量和 中使用的常量。uint64_tcountBits()x_record
1赞 stefaanv 3/25/2014 #5

我建议使用一个包含boost.dynamic_bitset的类、一个用于处理新值存储的计数器和一个用于通过小时/分钟访问的转换函数。

dynamic_set 可处理大多数要求:

  • 提升,没有 C++11
  • 紧凑的
  • 处理 24*60 位
  • 具有用于对设置位进行计数的 count() 函数
  • 返回用于存储为位的块
  • 没有 std::vector 的“容器”问题

评论

0赞 sbi 3/25/2014
听起来很有趣()。我将如何推动/弹出这样的野兽?+1
0赞 stefaanv 3/25/2014
只需对计数器进行递增和取模,并使用它来索引下一个位。第一次迭代后,最早的值将被覆盖。顺便说一句,boost 也有循环缓冲区,但没有针对位进行优化......
0赞 stefaanv 3/25/2014
实际上,该部分与 vector<bool> 解相同。
3赞 Marius Bancila 3/27/2014 #6

正如评论中建议的那样,可能是一个不错的选择。它是一个固定大小的位序列,可以独立操作。它比 a 占用更少的内存(即使你认为你说这对你来说不是问题)。但是,如果您需要使序列呈圆形,则可能需要将其包装在另一个容器中,以便始终保留最后 24*60 分钟,而不是一天中的 24*60 分钟。std::bitsetstd::vector<bool>

评论

0赞 stefaanv 3/27/2014
我建议boost.dynamic_bitset,因为我错误地认为 std::bitset 具有最大大小(如构造函数和 to_ulong() 函数所建议的那样),但事实并非如此,并且启动时知道位数,因此 std::bitset 更适合。
0赞 sbi 4/8/2014
最后,我发布(并接受了)我自己的答案,因为你的答案有点含糊不清,但你肯定把我带到了正确的方向。谢谢!
1赞 sbi 4/8/2014 #7

正如马里乌斯·班西拉(Marius Bancila)首先在评论中建议的那样,然后在答案中建议(请去投票支持该答案,他给出的提示最终是解决方案),是理想的选择。然而,由于他的回答相当模糊,我决定对我最终使用的内容进行更具体的描述:std::bitset<>

class x_record {
  public:
    void record_entry(bool x_occured) {
      record_ <<= 1;
      record_[0] = x_occurred;
    }

    unsigned int x_occurance_minutes() const {
      return record_.count();
    }

    void write_to(std::ostream& os) const {
      os << m_overload_minutes.to_string();
    }

    void read_from(std::istream& is) const {
      std::string bits;
      is >> std::setw(minutes_to_keep) >> bits;
      if( !is || bits.size()!=minutes_to_keep )
        throw std::runtime_error("invalid file format");

      record_ = std::bitset<60*24>(bits);
    }

  private:
    std::bitset<60*24>  record_;
};

正如你所看到的,这几乎正是我所需要的。此外,持久保存数据也非常容易。std::bitset<>

当然,实际上,这有点复杂1,但原则上这确实是整个事情。

感谢大家努力帮助我!

1 事实证明,每隔几毫秒调用一次更容易,这似乎有些开销,所以我调用了它(每分钟调用一次)并缓存了结果。x_occurance_minutes()record_.count()record_entry()