C语言中的滚动中位数算法

128

我目前正在编写一个用于在 C 中实现滚动中位数滤波器(类似于滚动平均滤波器)的算法。通过查阅文献,似乎有两种比较有效的方法来实现它。第一种方法是对初始值窗口进行排序,然后执行二分搜索以在每次迭代时插入新值并删除现有值。

第二种方法(来自 Hardle 和 Steiger,在 1995 年的 JRSS-C,算法 296 中)构建了一个双端堆结构,其中一个端点是最大堆,另一个端点是最小堆,并且中位数位于中间。这会产生一个线性时间算法,而不是 O(n log n) 的算法。

我的问题在于:虽然第一种方法可行,但我需要在数百万个时间序列上运行此程序,因此效率非常重要。而实现第二种方法却很困难。我在 R 统计软件包的 stats 包中的 Trunmed.c 文件中找到了代码,但它相当难以理解。

有人知道一个良好编写的 C 实现线性时间滚动中位数算法吗?

编辑:Trunmed.c 代码链接http://google.com/codesearch/p?hl=en&sa=N&cd=1&ct=rc#mYw3h_Lb_e0/R-2.2.0/src/library/stats/src/Trunmed.c


刚刚实现了移动平均值...移动中位数有点棘手。试着谷歌一下移动中位数。 - hookenz
每个时间序列中有多少个数字?即使有百万个时间序列,如果你只有几千个数字,如果你的代码写得高效,运行时间可能不会超过一两分钟。 - Dana the Sane
那段代码的参考资料太古老了!R 2.2.0已经过去三年了,我们现在使用的是R 2.9.1,而且预计将于9月24日发布2.9.2版本,10月份发布R 2.10.0。 - Dirk Eddelbuettel
18
两堆解法为什么是线性的?因为它的时间复杂度为O(n log k),其中k代表窗口大小,而堆的删除操作是O(log k)的。 - yairchu
5
一些实现和比较:https://github.com/suomela/median-filter - Jukka Suomela
显示剩余2条评论
13个回答

-1

这是Java的实现

package MedianOfIntegerStream;

import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;


public class MedianOfIntegerStream {

    public Set<Integer> rightMinSet;
    public Set<Integer> leftMaxSet;
    public int numOfElements;

    public MedianOfIntegerStream() {
        rightMinSet = new TreeSet<Integer>();
        leftMaxSet = new TreeSet<Integer>(new DescendingComparator());
        numOfElements = 0;
    }

    public void addNumberToStream(Integer num) {
        leftMaxSet.add(num);

        Iterator<Integer> iterMax = leftMaxSet.iterator();
        Iterator<Integer> iterMin = rightMinSet.iterator();
        int maxEl = iterMax.next();
        int minEl = 0;
        if (iterMin.hasNext()) {
            minEl = iterMin.next();
        }

        if (numOfElements % 2 == 0) {
            if (numOfElements == 0) {
                numOfElements++;
                return;
            } else if (maxEl > minEl) {
                iterMax.remove();

                if (minEl != 0) {
                    iterMin.remove();
                }
                leftMaxSet.add(minEl);
                rightMinSet.add(maxEl);
            }
        } else {

            if (maxEl != 0) {
                iterMax.remove();
            }

            rightMinSet.add(maxEl);
        }
        numOfElements++;
    }

    public Double getMedian() {
        if (numOfElements % 2 != 0)
            return new Double(leftMaxSet.iterator().next());
        else
            return (leftMaxSet.iterator().next() + rightMinSet.iterator().next()) / 2.0;
    }

    private class DescendingComparator implements Comparator<Integer> {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2 - o1;
        }
    }

    public static void main(String[] args) {
        MedianOfIntegerStream streamMedian = new MedianOfIntegerStream();

        streamMedian.addNumberToStream(1);
        System.out.println(streamMedian.getMedian()); // should be 1

        streamMedian.addNumberToStream(5);
        streamMedian.addNumberToStream(10);
        streamMedian.addNumberToStream(12);
        streamMedian.addNumberToStream(2);
        System.out.println(streamMedian.getMedian()); // should be 5

        streamMedian.addNumberToStream(3);
        streamMedian.addNumberToStream(8);
        streamMedian.addNumberToStream(9);
        System.out.println(streamMedian.getMedian()); // should be 6.5
    }
}

1
你应该提出一个新问题,然后在那个问题中提供你的Java答案。 - jww

-1

根据 @mathog 的想法,这是一个针对已知值范围的字节数组的C#实现运行中位数。可以扩展到其他整数类型。

  /// <summary>
  /// Median estimation by histogram, avoids multiple sorting operations for a running median
  /// </summary>
  public class MedianEstimator
  {
    private readonly int m_size2;
    private readonly byte[] m_counts;

    /// <summary>
    /// Estimated median, available right after calling <see cref="Init"/> or <see cref="Update"/>.
    /// </summary>
    public byte Median { get; private set; }

    /// <summary>
    /// Ctor
    /// </summary>
    /// <param name="size">Median size in samples</param>
    /// <param name="maxValue">Maximum expected value in input data</param>
    public MedianEstimator(
      int size,
      byte maxValue)
    {
      m_size2 = size / 2;
      m_counts = new byte[maxValue + 1];
    }

    /// <summary>
    /// Initializes the internal histogram with the passed sample values
    /// </summary>
    /// <param name="values">Array of values, usually the start of the array for a running median</param>
    public void Init(byte[] values)
    {
      for (var i = 0; i < values.Length; i++)
        m_counts[values[i]]++;

      UpdateMedian();
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void UpdateMedian()
    {
      // The median is the first value up to which counts add to size / 2
      var sum = 0;
      Median = 0;
      for (var i = 0; i < m_counts.Length; i++)
      {
        sum += m_counts[i];
        Median = (byte) i;
        if (sum > m_size2) break;
      }
    }

    /// <summary>
    /// Updates the median estimation by removing <paramref name="last"/> and adding <paramref name="next"/>. These
    /// values must be updated as the running median is applied. If the median length is <i>N</i>, at the sample
    /// <i>i</i>, <paramref name="last"/> is sample at index <i>i</i>-<i>N</i>/2 and <paramref name="next"/> is sample
    /// at index <i>i</i>+<i>N</i>/2+1.
    /// </summary>
    /// <param name="last">Sample at the start of the moving window that is to be removed</param>
    /// <param name="next">Sample at the end of the moving window + 1 that is to be added</param>
    public void Update(byte last, byte next)
    {
      m_counts[last]--;
      m_counts[next]++;

      // The conditions below do not change median value so there is no need to update it
      if (last == next ||
          last < Median && next < Median || // both below median
          last > Median && next > Median) // both above median
        return;

      UpdateMedian();
    }

测试运行中位数,计时:

private void TestMedianEstimator()
{
  var r = new Random();
  const int SIZE = 15;
  const byte MAX_VAL = 80;
  var values = new byte[100000];
  for (var i = 0; i < values.Length; i++)
    values[i] = (byte) (MAX_VAL * r.NextDouble());

  var timer = Stopwatch.StartNew();
  // Running median
  var window = new byte[2 * SIZE + 1];
  var medians = new byte[values.Length];
  for (var i = SIZE; i < values.Length - SIZE - 1; i++)
  {
    for (int j = i - SIZE, k = 0; j <= i + SIZE; j++, k++)
      window[k] = values[j];
    Array.Sort(window);
    medians[i] = window[SIZE];
  }
  timer.Stop();
  var elapsed1 = timer.Elapsed;

  timer.Restart();
  var me = new MedianEstimator(2 * SIZE + 1, MAX_VAL);
  me.Init(values.Slice(0, 2 * SIZE + 1));
  var meMedians = new byte[values.Length];
  for (var i = SIZE; i < values.Length - SIZE - 1; i++)
  {
    meMedians[i] = me.Median;
    me.Update(values[i - SIZE], values[i + SIZE + 1]);
  }
  timer.Stop();
  var elapsed2 = timer.Elapsed;

  WriteLineToLog($"{elapsed1.TotalMilliseconds / elapsed2.TotalMilliseconds:0.00}");

  var diff = 0;
  for (var i = 0; i < meMedians.Length; i++)
    diff += Math.Abs(meMedians[i] - medians[i]);
  WriteLineToLog($"Diff: {diff}");
}

-6
如果您只需要平滑平均值,一种快速/简单的方法是将最新值乘以x和平均值乘以(1-x),然后相加。这就成为了新的平均值。
编辑:虽然不是用户要求的统计学上有效的方式,但对于许多用途来说已经足够好了。
我会将其保留在这里(尽管它遭受了反对票)供大家搜索!

2
这个计算的是平均值。他想要中位数。此外,他正在计算一个滑动窗口值的中位数,而不是整个集合的中位数。 - A. Levy
1
这个计算一个数值窗口的滑动平均值,衰减常数取决于X - 在性能要求高且无法使用卡尔曼滤波时非常有用。我加入了它以便搜索可以找到。 - Martin Beckett
这也是我立刻想到的,因为我曾经在一个音频应用程序中实现过这样的过滤器,作为一种非常基本和廉价的低通滤波器。 - James Morris

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