10

我需要从双精度数组中找到 n 最低的(不是 0)(我们称之为数组samples)。我需要在循环中多次执行此操作,因此执行速度至关重要。我尝试先对数组进行排序,然后取前 10 个值(不是 0),然而,虽然 Array.Sort 据说很快,但它成为了瓶颈:

const int numLowestSamples = 10;

double[] samples;

double[] lowestSamples = new double[numLowestSamples];

for (int count = 0; count < iterations; count++) // iterations typically around 2600000
{
    samples = whatever;
    Array.Sort(samples);
    lowestSamples = samples.SkipWhile(x => x == 0).Take(numLowestSamples).ToArray();
}

因此,我尝试了一个不同但不太干净的解决方案,首先读取前 n 个值,对它们进行排序,然后循环遍历样本中的所有其他值,检查该值是否小于排序后的最低样本数组中的最后一个值。如果该值较低,则将其替换为数组中的值并再次对数组进行排序。结果证明这快了大约 5 倍:

const int numLowestSamples = 10;

double[] samples;

List<double> lowestSamples = new List<double>();

for (int count = 0; count < iterations; count++) // iterations typically around 2600000
{
    samples = whatever;

    lowestSamples.Clear();

    // Read first n values
    int i = 0;
    do
    {
        if (samples[i] > 0)
            lowestSamples.Add(samples[i]);

        i++;
    } while (lowestSamples.Count < numLowestSamples)

    // Sort the array
    lowestSamples.Sort();

    for (int j = numLowestSamples; j < samples.Count; j++) // samples.Count is typically 3600
    {
        // if value is larger than 0, but lower than last/highest value in lowestSamples
        // write value to array (replacing the last/highest value), then sort array so
        // last value in array still is the highest
        if (samples[j] > 0 && samples[j] < lowestSamples[numLowestSamples - 1])
        {
            lowestSamples[numLowestSamples - 1] = samples[j];
            lowestSamples.Sort();
        }
    }
}

虽然这工作相对较快,但我想挑战任何人提出更快更好的解决方案。

4

6 回答 6

3

这称为选择算法。

此 Wiki 页面上有一些通用解决方案:

http://en.wikipedia.org/wiki/Selection_algorithm#Selecting_k_smallest_or_largest_elements

(但您必须做一些工作才能转换为 c#)

您可以使用 QuickSelect 算法找到第 n 个最低的元素,然后遍历数组以获取每个元素 <= 那个元素。

这里有一个 C# 中的 QuickSelect 示例:http: //dpatrickcaldwell.blogspot.co.uk/2009/03/more-ilist-extension-methods.html

于 2012-06-26T14:50:02.317 回答
2

理想情况下,您只想对集合进行一次传递,因此您的解决方案非常巧妙。但是,当您只需要提升其前面的数字时,您会在每次插入时使用整个子列表。但是,对 10 个元素进行排序几乎可以忽略不计,增强它并不会真正给您带来太多好处。对于您的解决方案,最坏的情况(就浪费的性能而言)是如果您从一开始就有 9 个最低的数字,因此对于您发现的每个后续数字 < lowestSamples[numLowestSamples - 1],您将对一个已经排序的列表进行排序(这是最坏的情况快速排序的场景)。

最重要的是,由于您使用的数字很少,因此考虑到使用托管语言执行此操作的开销,您无法进行很多数学改进。

对酷算法的赞誉!

于 2012-06-26T14:49:50.240 回答
2

无需重复排序最低样本,只需将样本插入它所在的位置:

int samplesCount = samples.Count;

for (int j = numLowestSamples; j < samplesCount; j++)
{
    double sample = samples[j];

    if (sample > 0 && sample < currentMax)
    {
        int k;

        for (k = 0; k < numLowestSamples; k++)
        {
           if (sample < lowestSamples[k])
           {
              Array.Copy(lowestSamples, k, lowestSamples, k + 1, numLowestSamples - k - 1);
              lowestSamples[k] = sample;

              break;
           }
        }

        if (k == numLowestSamples)
        {
           lowestSamples[numLowestSamples - 1] = sample;
        }

        currentMax = lowestSamples[numLowestSamples - 1];
    }
}

现在,如果 numLowestSamples 需要非常大(接近 samples.count 的大小),那么您可能希望使用可能更快的优先级队列(通常为 O(logn) 来插入新样本而不是 O(n/ 2) 其中 n 是 numLowestSamples)。优先级队列将能够有效地插入新值并在 O(logn) 时间敲掉最大值。

当 numLowestSamples 为 10 时,真的不需要它——特别是因为您只处理双精度数而不是复杂的数据结构。对于堆和小 numLowestSamples,为堆节点(大多数优先级队列使用堆)分配内存的开销可能大于任何搜索/插入效率增益(测试很重要)。

于 2012-06-26T15:17:49.223 回答
2

两种不同的想法:

  1. 无需对数组进行排序,只需对其执行一次插入排序。您已经知道新添加的项目将是唯一未排序的项目,因此请使用该知识。
  2. 看看堆排序。它构建一个二进制最大堆(如果你想从最小到最大排序),然后通过将索引 0 处的最大元素与仍然是堆的一部分的最后一个元素交换来开始从堆中删除元素。现在,如果您假装从最大到最小的元素对数组进行排序,则可以在对 10 个元素进行排序后停止排序。数组末尾的 10 个元素将是最小的,剩余的数组仍然是数组表示中的二进制堆。我不确定这将如何与Wikipedia 上基于 Quicksort 的选择算法进行比较。无论您要选择多少个元素,都将始终为整个数组构建堆。
于 2012-06-26T15:33:53.800 回答
2

我认为你的想法是正确的。即,一次通过并保持最小大小的排序数据结构通常是最快的。您对此的性能改进是优化。

您的优化将是:1)您每次通过都对结果进行排序。这对于小尺寸可能是最快的,但对于较大的套装来说不是最快的。考虑可能有两种算法,一种用于低于给定阈值,另一种(如堆排序)用于高于阈值。2) 跟踪必须从最小集合中删除的任何值(您当前通过查看最后一个元素来执行此操作)。您可以跳过插入和排序任何大于或等于被踢出的任何值的值。

于 2012-06-26T15:51:12.010 回答
1

我认为您可能想尝试维护最小堆并测量性能差异。这是我一直在研究的称为斐波那契堆的数据结构。它可能需要做一些工作,但你至少可以检验我的假设。

public sealed class FibonacciHeap<TKey, TValue>
{
    readonly List<Node> _root = new List<Node>();
    int _count;
    Node _min;

    public void Push(TKey key, TValue value)
    {
        Insert(new Node {
            Key = key,
            Value = value
        });
    }       

    public KeyValuePair<TKey, TValue> Peek()
    {
        if (_min == null)
            throw new InvalidOperationException();
        return new KeyValuePair<TKey,TValue>(_min.Key, _min.Value);
    }       

    public KeyValuePair<TKey, TValue> Pop()
    {
        if (_min == null)
            throw new InvalidOperationException();
        var min = ExtractMin();
        return new KeyValuePair<TKey,TValue>(min.Key, min.Value);
    }

    void Insert(Node node)
    {
        _count++;
        _root.Add(node);
        if (_min == null)
        {
            _min = node;
        }
        else if (Comparer<TKey>.Default.Compare(node.Key, _min.Key) < 0)
        {
            _min = node;
        }
    }

    Node ExtractMin()
    {
        var result = _min;
        if (result == null)
            return null;
        foreach (var child in result.Children)
        {
            child.Parent = null;
            _root.Add(child);
        }
        _root.Remove(result);
        if (_root.Count == 0)
        {
            _min = null;
        }
        else
        {
            _min = _root[0];
            Consolidate();
        }
        _count--;
        return result;
    }

    void Consolidate()
    {
        var a = new Node[UpperBound()];
        for (int i = 0; i < _root.Count; i++)
        {
            var x = _root[i];
            var d = x.Children.Count;
            while (true)
            {   
                var y = a[d];
                if (y == null)
                    break;                  
                if (Comparer<TKey>.Default.Compare(x.Key, y.Key) > 0)
                {
                    var t = x;
                    x = y;
                    y = t;
                }
                _root.Remove(y);
                i--;
                x.AddChild(y);
                y.Mark = false;
                a[d] = null;
                d++;
            }
            a[d] = x;
        }
        _min = null;
        for (int i = 0; i < a.Length; i++)
        {
            var n = a[i];
            if (n == null)
                continue;
            if (_min == null)
            {
                _root.Clear();
                _min = n;
            }
            else
            {
                if (Comparer<TKey>.Default.Compare(n.Key, _min.Key) < 0)
                {
                    _min = n;
                }
            }
            _root.Add(n);
        }
    }

    int UpperBound()
    {
        return (int)Math.Floor(Math.Log(_count, (1.0 + Math.Sqrt(5)) / 2.0)) + 1;
    }

    class Node
    {
        public TKey Key;
        public TValue Value;
        public Node Parent;
        public List<Node> Children = new List<Node>();
        public bool Mark;

        public void AddChild(Node child)
        {
            child.Parent = this;
            Children.Add(child);
        }

        public override string ToString()
        {
            return string.Format("({0},{1})", Key, Value);
        }
    }
}
于 2012-06-26T14:47:17.510 回答