【问题标题】:Improving performance of a Nested Loop提高嵌套循环的性能
【发布时间】:2013-06-10 20:46:22
【问题描述】:

此逻辑是在数组中查找数字 n,其中 n 和 n + 5 之间的范围将包含数组中最多的数字。我想出了一个解决方案,但它需要一个嵌套循环,因此它有点慢。有什么方法可以提高它的性能吗?提前致谢。

保证数组是有序的。

int[] myArray = new int[]{1,2,4,5,7,9,15,19};
int bestNumber = 0;
int MaxMatchFound = 0;

for (int o = 0; o < myArray.Length; o++)
{

    int TempMatchFound = 0;

    for (int i = 0; i < myArray.Length; i++)
    {
        if (myArray[i] >= myArray[o] && myArray[i] <= (myArray[o] + 5))
        {
            TempMatchFound++;
        }
    }
    if (TempMatchFound > MaxMatchFound)
    {
        bestNumber = myArray[o];
        MaxMatchFound = TempMatchFound;
    }

}

return bestNumber;

【问题讨论】:

  • 它需要执行多快,您将为此使用多大的数据集?
  • 你能保证数组最初是排序的吗?
  • 请注意,如果您确保输入数据已排序,则可以在值大于上限时立即跳出内循环,并且可以在当前项目,而不是开始时。
  • @JoelCoehoorn 是的,它已排序。我忘了提。
  • 另外,解释@Servy 的第一条评论:性能是相对昂贵。相对而言,对于样本数据集的大小,您现在拥有的已经足够快了。昂贵,因为程序员的时间通常比 CPU 时间更昂贵(而且稀缺)。优化此代码所花费的时间是以优化其他代码为代价的。如果您需要优化某些东西,您是否将时间花在此处或其他地方取决于您需要查看多少数据以及 cpu 可以花多长时间。

标签: c# arrays performance sorting loops


【解决方案1】:

存储这些值,然后循环遍历值v,并对满足v &lt;= w &lt;= v + 5 的所有值w 的相关计数求和,然后找到最大计数:

var buckets = myArray.GroupBy(n => n)
                     .ToDictionary(g => g.Key, g => g.Count());
var rangeCounts = 
    buckets.Keys
           .Select(v =>
               new {
                   Value = v,
                   Count = Enumerable.Range(0, 6)
                                     .Sum(i => buckets.ContainsKey(v + i) ? 
                                               buckets[v + i] : 
                                               0
                                         )
               }
    );
var bestRange = rangeCounts.MaxBy(x => x.Count);

现在,bestRange.Value 是最佳范围的起点,bestRange.Count 是落入范围 [bestRange.Value, bestRange.Value + 5] 的元素数。在这里,我使用了MaxBy

认为这可以为您带来线性性能。构建字典是线性的,构建 rangeCounts 是线性的,MaxBy 是线性的。甚至适用于非排序。

【讨论】:

  • @ghord 那是什么?将单个项目添加到字典需要 O(1) 时间。 (摊销时,它可以在这里。)你这样做 N 次,那是 O(N)。
  • +1 一个有趣的解决方案。我想知道它是否可以在恒定的额外空间中完成。
  • 是的,它可以在 O(n) 中使用 O(1) 常量额外空间来完成,无论范围如何。看我的回答。
【解决方案2】:

给你:这在 O(N) 时间和 O(1) 内存中运行。这形成了其他解决方案中描述的存储桶,然后在您通过阵列时丢弃它们。队列用于跟踪哪些桶是“活动的”,因为它们可以被添加到。字典中的条目永远不会超过 6 个,队列也不会。

int[] myArray = new int[]{1,2,4,5,7,9,15,19};
Dictionary<int, int> counts = new Dictionary<int, int>();
Queue<int> q = new Queue<int>();

int n = 0;
int currentMaxCount = 0;


for(int i = 0; i < myArray.Length; i++)
{
    var currentNum = myArray[i];
    if(counts.ContainsKey(currentNum))
    {
        counts[currentNum]++;
    }
    else
    {
        counts[currentNum] = 1;
        q.Enqueue(currentNum);
    }

    for(int j = 1; j <= 5; j++)
    {
        if(counts.ContainsKey(currentNum - j))
            counts[currentNum - j]++;
    }

    if(q.Peek() + 5 < currentNum)
    {
        if(counts[q.Peek()] > currentMaxCount)
        {
            currentMaxCount = counts[q.Peek()];
            n = q.Peek();
        }
        counts.Remove(q.Dequeue());

    }
}

while(q.Count > 0)
{
    if(counts[q.Peek()] > currentMaxCount)
    {
        currentMaxCount = counts[q.Peek()];
        n = q.Peek();
    }
    counts.Remove(q.Dequeue());
}

Console.WriteLine("There are {0} matches between {1} and {2}", currentMaxCount, n, n + 5);

【讨论】:

  • 使用嵌套循环,看起来将是 n^2。无论如何,这也需要 O(n) 额外空间。不一定是坏事,但你应该指出这一点。
  • 这不是 O(n^2) 因为该范围内的元素少于 5 个。该嵌套循环实际上是 O(1)。
  • 只有在不允许重复时才如此。
  • 已更新以在重复的情况下保持 O(n) 时间和正确性。
  • 进一步更新为仅使用常量空间。
【解决方案3】:

这是一个 O(n) 的解决方案,并且无论范围如何都使用 O(1) 额外空间。

它只遍历数组一次,总是进行 2N 次比较。我看不出有什么方法可以改进这个算法,尽管肯定有一些微优化可以提高实现的速度。

private int FindRange(int[] myArray)
{
    const int range = 5;
    int start = 0;
    int maxMatchFound = 0;
    int maxIndex = 0;
    for (int i = 0; i < myArray.Length; ++i)
    {
        if (myArray[i] > myArray[start] + range)
        {
            int matchLength = i - start;
            if (matchLength > maxMatchFound)
            {
                maxMatchFound = matchLength;
                maxIndex = start;
            }
            // move forward until within range
            do
            {
                ++start;
            } while (myArray[i] > myArray[start] + range);
        }
    }
    // Final check, from myArray[start] to end of array
    int len = myArray.Length - start;
    if (len > maxMatchFound)
    {
        maxMatchFound = len;
        maxIndex = start;
    }
    return maxIndex;

这里的想法是,如果特定数字a[x] 落在a[i] 的范围内,那么它将在a[i+1] 的范围内,假设x &gt; i。 (所以在你的原始数组中,a[3] 的值在a[0] 的范围内,所以它也会在a[1]a[2] 的范围内。

所以索引i 会递增,直到它引用的值超出a[start] 的范围。然后,start 递增,直到 a[i] 再次在范围内。两个索引以这种交替方式在数组中向前移动。

【讨论】:

  • 不起作用:int[] myArray = new int[]{1,2,2,3,3,3,4,4,4,4,5,5,5,5,5,6,6,6,6,6,6,7,7,7,7,7,7,7}; 返回 0,应该返回 1(或 2)。
  • 我试图向它抛出一些可怕的数据,它在 O(n) 中运行。代码有点难以理解。 var myArray = Enumerable.Range(1, 10000).SelectMany (i =&gt; Enumerable.Range(1, i).Select (j =&gt; i)).ToArray(); 生成了一个包含 5000 万个项目的金字塔数组,它运行了大约 1 亿次。
  • @Shlomo:也许你会发现新版本更容易理解。
【解决方案4】:

这是一个单行 LINQ 选项。在性能方面不是最好的(它迭代多次)。仍然值得注意。

var result = myArray
             .OrderByDescending(i => myArray.Count(i2 => i2 >= i && i2 <= i + 5))
             .First();

【讨论】:

  • 他专门问如何提高性能。虽然这种方法的代码要少得多,但它比 OP 已有的方法要慢,因此这是一个非常糟糕的答案。
猜你喜欢
  • 2013-01-29
  • 1970-01-01
  • 1970-01-01
  • 2012-11-25
  • 2021-12-01
  • 2019-04-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多