【问题标题】:find median in a fixed-size moving window along a long sequence of data沿长数据序列在固定大小的移动窗口中找到中值
【发布时间】:2012-04-08 03:27:09
【问题描述】:

给定一个数据序列(它可能有重复),一个固定大小的移动 窗口,在每次迭代时从数据开始移动窗口 序列,使得 (1) 最旧的数据元素被从窗口中移除,一个新的数据元素被移除 元素被推入窗口 (2) 求每次移动时窗口内数据的中位数。

以下帖子没有帮助。

Effectively to find the median value of a random sequence

joining data based on a moving time window in R

我的想法:

使用 2 个堆来保存中位数。在窗口旁边,对窗口中的数据进行排序 在第一次迭代中,最小堆包含较大的部分和最大堆 持有较小的部分。如果窗口有奇数个数据,最大堆 返回中位数,否则返回顶部元素的算术平均值 两堆是中位数。

当一个新数据被推入窗口时,从一个窗口中删除最旧的数据 堆并将新数据与最大和最小堆的顶部进行比较,所以 决定要放置数据的堆。然后,找到中位数 就像第一次迭代一样。

但是,如何在堆中找到数据元素是一个问题。堆是二进制的 树不是二叉搜索树。

是否可以用 O(n) 或 O(n * lg m) 来解决它,其中 m 是窗口大小并且 空间:O(1) ?

非常感谢任何帮助。

谢谢

【问题讨论】:

  • 下一项是最新数据,还是有其他条件?您是否以先进先出的方式处理这些项目?
  • @Glenn,每次迭代,从窗口中删除最旧的数据,并在窗口中放入新数据,然后在窗口中找到新的中位数。对于最旧的数据,它是 FIFO。谢谢
  • 我不认为,空间 O(1) 是可能的。您需要存储窗口内容,因此您不会低于 O(m)。
  • 如何从一个堆中删除最旧的数据?

标签: c++ algorithm data-structures median


【解决方案1】:

O(n*lg m) 很简单:

只需将您的窗口保持为两个std::sets,一个用于下半部分,一个用于上半部分。插入新元素的成本为 O(lg m),查找和移除旧元素的成本相同。使用您在问题中描述的方法确定中位数的成本为 O(1)。

当您在序列上滑动窗口时,在每次迭代中,您删除掉出窗口的项目 (O(lg m)),插入新项目 (O(lg m)) 并计算中位数 (O( 1)),总共有 O(n lg m)。

当然,这个解决方案使用空间 O(m),但我认为不存储窗口内容就无法逃脱。

【讨论】:

  • stl:set 是一个二叉搜索树。堆是二叉树。您可以使用 O(1) 在堆顶部找到中值,但您不能在 O(1) 内找到二叉搜索树中的中值,因为二叉搜索树顶部的顶部元素可能不是最大值或树的最小值(与堆不同)。谢谢!
  • @user1002288:他说使用两个 BST - 一个用于较低的m/2 - 1 元素,一个用于较高的m/2 - 1 元素,并且还存储中位数。当您将窗口移到上方时:1. 从下层树或上层树 (O(log m)) 中删除旧元素,然后 2. 将新元素插入下层树或上层树,取决于它是 中值。如果其中一棵树现在有太多元素,请删除最大的 (对于下部) 或最小的 (对于上部) 元素(O(log m)),称为新的中位数, 并将旧中位数插入另一棵树 (O(log m))。总计O(log m)
  • @user1002288 另外,找到二叉树的最小值/最大值可以O(1) - 只需保留对它们的引用即可。 C++ 的stl::set 已经这样做了,通过*stl::set::rbegin()
  • @BlueRaja,这是个好主意。但是,可能有重复。例如3 , 6 , 6 , 7 , 9 ,如果 6 中的一个是最旧的并且它被删除,但仍然剩下另一个 6。在 stl:set 中,不允许重复。如果我们使用multiset,同一个key关联的所有数据都会被删除。这意味着两个6s都将被删除。这是错误的。谢谢。
  • @user1002288:不,只需使用允许重复的 BST,例如。左子树不是< 父子树和右子树>,而是左子树<= 和右>。每个节点仍应有两个 (或更少) 个子节点。
【解决方案2】:

答案与 hc_ 相同,但不是使用库存 BST,而是使用每个节点都具有该子树中元素计数的版本。这种求中位数的方法是 O(log(m))。

【讨论】:

    【解决方案3】:

    我几乎完全实现了您在此处描述的算法:http://ideone.com/8VVEa,并在此处描述了它:Rolling median in C - Turlach implementation

    解决“查找最旧”问题的方法是将值保存在循环缓冲区中,因此您始终有一个指向最旧的指针。您存储在堆中的是缓冲区索引。 所以空间需求是2M,每次更新是O(lg M)。

    【讨论】:

      【解决方案4】:

      我为“C 中的滚动中位数”问题给出了这个答案

      我找不到具有 order-statistic 的 c++ 数据结构的现代实现,因此最终在顶级编码器链接中实现了这两个想法(Match Editorial:向下滚动到 FloatingMedian)。

      两个多组

      第一个想法将数据划分为两个数据结构(堆、多集等),每次插入/删除 O(ln N) 不允许在没有大成本的情况下动态更改分位数。 IE。我们可以有滚动的中位数,或者滚动的 75%,但不能同时使用两者。

      段树

      第二个想法使用 O(ln N) 的段树进行插入/删除/查询,但更灵活。最好的“N”是数据范围的大小。因此,如果您的滚动中位数具有一百万个项目的窗口,但您的数据与 1..65536 不同,那么每次移动 100 万个滚动窗口只需要 16 次操作! (而且您只需要 65536 * sizeof(counting_type) 个字节,例如 65536*4)。

      GNU 顺序统计树

      就在放弃之前,发现stdlibc++中包含顺序统计树!!!

      这些有两个关键操作:

      iter = tree.find_by_order(value)
      order = tree.order_of_key(value)
      

      参见libstdc++ manual policy_based_data_structures_test(搜索“拆分并加入”)。

      我已经包装了树,以便在支持 c++0x/c++11 样式部分类型定义的编译器的便利标头中使用:

      #if !defined(GNU_ORDER_STATISTIC_SET_H)
      #define GNU_ORDER_STATISTIC_SET_H
      #include <ext/pb_ds/assoc_container.hpp>
      #include <ext/pb_ds/tree_policy.hpp>
      
      // A red-black tree table storing ints and their order
      // statistics. Note that since the tree uses
      // tree_order_statistics_node_update as its update policy, then it
      // includes its methods by_order and order_of_key.
      template <typename T>
      using t_order_statistic_set = __gnu_pbds::tree<
                                        T,
                                        __gnu_pbds::null_type,
                                        std::less<T>,
                                        __gnu_pbds::rb_tree_tag,
                                        // This policy updates nodes'  metadata for order statistics.
                                        __gnu_pbds::tree_order_statistics_node_update>;
      
      #endif //GNU_ORDER_STATISTIC_SET_H
      

      【讨论】:

        【解决方案5】:

        我附上了我的分段树(参见我的另一篇文章),它可以非常有效地查询计数的频率分布。

        这实现了以下数据结构:

        |-------------------------------|
        |---------------|---------------|
        |-------|-------|-------|-------|
        |---|---|---|---|---|---|---|---|
          0   1   2   3   4   5   6   7  
        

        每个段都将计数项目的数量保持在其涵盖的范围内。 我将 2N 段用于 1..N 的值范围。 它们被放置在一个展开的向量中,而不是上面形象地显示的树格式。

        因此,如果您要计算一组从 1..65536 变化的整数的滚动中位数,那么您只需要 128kb 来存储它们,并且可以使用 O(ln N) 插入/删除/查询,其中 N = 大小范围,即 2**16 次操作。

        如果数据范围远小于您的滚动窗口,这是一个巨大的胜利。

        #if !defined(SEGMENT_TREE_H)
        #define SEGMENT_TREE_H
        #include <cassert>
        #include <array>
        #include <algorithm>
        #include <set>
        
        #ifndef NDEBUG
        #include <set>
        #endif
        
        template<typename COUNTS, unsigned BITS>
        class t_segment_tree
        {
            static const unsigned                       cnt_elements    = (1 << BITS);
            static const unsigned                       cnt_storage     = cnt_elements << 1;
            std::array<COUNTS, cnt_elements * 2 - 1>    counts;
            unsigned                                    count;
        
        #ifndef NDEBUG
            std::multiset<unsigned>                     elements;
        #endif
            public:
        
            //____________________________________________________________________________________
        
            //  constructor
        
            //____________________________________________________________________________________
            t_segment_tree(): count(0)
            {
                std::fill_n(counts.begin(), counts.size(),  0);
            }
            //~t_segment_tree();
        
            //____________________________________________________________________________________
        
            //  size
        
            //____________________________________________________________________________________
            unsigned size() const  { return count; }
        
            //____________________________________________________________________________________
        
            //  constructor
        
            //____________________________________________________________________________________
            void insert(unsigned x)
            {
        #ifndef NDEBUG
                elements.insert(x);
                assert("...............This element is too large for the number of BITs!!..............." && cnt_elements > x);
        #endif
                unsigned ii = x + cnt_elements;
                while (ii)
                {
                    ++counts[ii - 1];
                    ii >>= 1;
                }
                ++count;
            }
        
            //____________________________________________________________________________________
        
            //  erase 
        
            //      assumes erase is in the set
            //____________________________________________________________________________________
            void erase(unsigned x)
            {
        #ifndef NDEBUG
                // if the assertion failed here, it means that x was never "insert"-ed in the first place
                assert("...............This element was not 'insert'-ed before it is being 'erase'-ed!!..............." && elements.count(x));
                elements.erase(elements.find(x));
        #endif
                unsigned ii = x + cnt_elements;
                while (ii)
                {
                    --counts[ii - 1];
                    ii >>= 1;
                }
                --count;
            }
        
            // 
            //____________________________________________________________________________________
        
            //  kth element
        
            //____________________________________________________________________________________
            unsigned operator[](unsigned k)
            {
                assert("...............The kth element: k needs to be smaller than the number of elements!!..............." && k < size());
                unsigned ii = 1;
                while (ii < cnt_storage)
                {
                    if (counts[ii - 1] <= k)
                       k -= counts[ii++ - 1];
                    ii <<= 1;
                }
                return (ii >> 1) - cnt_elements;
            }
        
        };
        #endif
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2016-09-06
          • 2020-03-29
          • 1970-01-01
          • 1970-01-01
          • 2021-09-20
          • 2019-03-02
          • 1970-01-01
          相关资源
          最近更新 更多