【问题标题】:C# Most efficient data structure to insert and to remove lower halfC#最有效的数据结构插入和删除下半部分
【发布时间】:2016-03-31 10:44:56
【问题描述】:

想象一下,我有一个大的整数排序列表(>1000 个项目)。我需要能够对此列表执行两项操作:删除下半部分并通过插入随机整数再次将列表填充到其原始大小。因为我做了大约一百万次这些操作,所以我需要它尽可能高效。

我做的第一件事就是使用List,通过在正确的位置添加新项目来保持排序。虽然删除排序列表的下半部分很容易,但插入需要相当长的时间。

我尝试实现一个跳过列表,但经过一些测试后,似乎列表的大小必须至少为 10 000 才真正重要,否则它的性能甚至比我的正常列表更差。

这就是我决定使用 AVL 树的原因,这样我可以更快地插入项目。但问题是我不知道有什么有效的方法可以删除这种二叉搜索树的下半部分。

我的问题是:有没有一种有效的方法来做到这一点?还有其他我可以更轻松地使用的数据结构吗?

编辑

按照要求,我做了一个小测试,显示了列表、跳过列表和 AVL 树之间的性能差异。我使用 msdn 上的本教程制作了跳过列表:Skip list tutorial。 AVL 树来自这里:AVL tree。我在 Pastebin 上上传了测试:Program

在测试中,我在计时时向每个数据结构添加了 100 000 个项目。在我的电脑上,该列表耗时约 1 秒,跳过列表耗时 0.5 秒,AVL 树耗时 0.045 秒。如果我想这样做一百万次,列表将需要大约 11.5 天,但 AVL 树只需要大约半天。这个时间差清楚地说明了为什么我希望它高效。

【问题讨论】:

  • 如果您使用平衡的 AVL 树,您只需删除左侧即可删除数字较小的一半。
  • 我一开始也是这么想的,但是虽然树是平衡的,但不代表树的左右两边大小完全一样。例如,如果你有一棵包含偶数个项目的树,那甚至是不可能的,因为根将有奇数个孩子分成两部分。
  • 但是无论你使用什么数据结构,你仍然会遇到偶数/奇数项目的同样问题。只是为了确保:您要删除所有低于项目中值的数字,对吗?
  • 是的,这是正确的,但奇数/偶数大小并不是树可以具有的唯一大小差异。搜索树的属性之一是两个兄弟姐妹的高度最多相差 1,但如果我没记错的话,这并不意味着大小只能相差 1。另一个例子:树的左侧可以有 3 的高度,末端有 4 片叶子,而树的右侧可以有 2 的高度,末端只有 2 片叶子。左侧总共有 7 个孩子,而右侧只有 3 个。
  • @Safron 想到三个想法,但它们取决于您的具体要求。首先是听起来你正在创建一种(二进制)堆结构......这不能解决问题吗?其次,如果您只使用完整的树而不是部分树,您可能只是填充一个数组并对其进行排序(不是每个值,而是在插入所有内容之后)。第三是您不需要在第二种情况下对所有内容进行排序,而只需对新值进行排序,并改用双向合并连接。无论如何,这完全取决于您的具体要求,所以请详细说明,以便我考虑。

标签: c# data-structures binary-search-tree avl-tree


【解决方案1】:

关于这个问题,我想指出几点。首先,让我们直接了解一些关于性能和 C# 的总体情况,因为在仍然存在误解的情况下很难解释这些内容。

接下来,我将在此处将所有内容应用于特定问题。

一般的 C# 性能

大 O 表示法

在大学里,你会了解到 O(n) 总是优于 O(n^2) 以及 O(n) 总是优于 O(n log n)。但是,对此的基本假设是每个操作将花费大致相同的时间。

现在,当我在 1986 年第一次开始在 1802 RISC 处理器上编程时,情况非常普遍:内存操作是 1 个时钟滴答,加法、减法等也是如此。换句话说,Big-O在那里工作得很好。

在现代计算机中,这更困难:

  1. 数据缓存在不同级别(速度范围从 15 GB/s 到 1000 GB/s);
  2. 操作在 0.5 个时钟节拍和几十个时钟节拍之间变化;
  3. 数据通常以突发方式获取 - 因此随机访问比顺序访问要糟糕得多;
  4. 向量化可以一次处理多达 8 个整数的对齐数据;
  5. 分支错误预测可能会使一切失去平衡,因为您必须刷新批次。

我观察到同一算法的不同实现的性能差异可能高达 1000 倍(!)

Big-O 仍然具有优点,但你应该正确看待事情。例如,假设你有 N=10000,然后是 2log N ~ 13——如果这意味着你可以从所有这些东西中受益,这也可能意味着一个“愚蠢”的 O(n log n) 算法可能会胜过你的平均 O(n) 算法。

据此,您还应该推断出 O(n^2) 永远不会胜过 O(n) 算法。所以,Big-O 仍然有它的用途;你只需要正确看待事物。

关于 C# 的一些特点

关于 C# 的一个神话是它大约与 C++ 一样快(这是我的“尽可能快”的黄金标准)。在熟练的开发人员手中,事情并非如此简单。对于简单的排序,C++ 的速度大约是 2 倍 - 但如果您有更复杂的场景,您可以真正从“低级东西”中受益,差异可能会变得非常大。我通常估计性能差异是 10 倍。但是,编写正确的高性能 C++ 代码具有挑战性(使用轻描淡写的话),因此您可能希望坚持使用 C# 并决定将性能损失视为理所当然。

有趣的一点是 C# 编译器和 JIT 编译速度非常快。在某种程度上,这是因为它们按函数编译所有内容(因此,没有内联等)。此外,C# 通常不会对内容进行矢量化。不要相信我的话,在 Visual Studio 中使用 ctrl-alt-d 并自己检查汇编程序输出。

如果我们看一下上面的列表,我们可以粗略地说 (1)、(2) 和 (3) 不受我们使用 C# 的影响; (4) 肯定受到影响,(5) 依赖。

至于(5),考虑这个简单的例子:

void set(int[] array, int index) 
{
    array[index] = 0;
}

请记住,在 C# 中,方法是按方法编译的。这意味着编译器不能假定index 不会越界。换句话说:它必须添加两项检查 - 其中一项必须加载内存:

if (index < 0 || index >= array.Length) 
{ 
    throw new IndexOutOfRangeException(); 
}

排序项目

OP 的问题是关于维护大小为m 的排序列表。排序是一个众所周知的操作,最多将花费O(log m) 您插入的每个项目。由于您正在处理n“随机”项目,因此您将获得O(n log m) 的最佳速度。

二进制堆(基于数组)可能会为您提供该性能数字,但我现在不想写下堆,并认为这种替代方案的速度大致相同(如果不是更快的话):)

您的问题

既然我们已经确定了我们要讨论的内容,那么让我们来看看手头的事实吧。我将在每一步中解释这一点。

首先,在处理性能方面的问题时,我养成了删除using System.Linq 的习惯,因此我们知道我们只是在处理具有预期特征的本机数据结构。

让我们从树形结构开始

另一个简单的解决方案是使用红黑树。我们在 .NET 中有一个供我们使用,称为 SortedSet。它使用引用、算术等——这基本上是我在 (1)、(2) 和 (3) 中警告过的所有讨厌的东西。现在,这里的实现中存在错误(对于重复),但速度几乎是您所期望的:

static void Main(string[] args)
{
    Random rnd = new Random(12839);

    SortedSet<int> list = new SortedSet<int>();

    for (int i = 0; i < 5000; ++i)
    {
        list.Add(rnd.Next());
    }

    Stopwatch sw = Stopwatch.StartNew();
    for (int i = 0; i < 10000; ++i)
    {
        for (int j = 0; j < 5000; ++j)
        {
            list.Add(rnd.Next());
        }
        int n = 0;
        list.RemoveWhere((a) => n++ < 5000);
    }

    Console.WriteLine(sw.ElapsedMilliseconds);

    Console.ReadLine();
}

与此处的几乎所有算法一样,此算法在 O(n log m) 中执行。

我对 AVL 树的大致期望:86220 毫秒。

简单实现

通常我不会为红黑树而烦恼。不过,由于您在 AVL 树中投入了大量工作,我觉得有必要进行此测量。

当我对算法进行性能优化时,我总是从最容易实现的算法开始,该算法具有大约正确的 Big-O,并且总是更喜欢具有最简单数据结构的算法(读取:数组)。在这种情况下,它是一个与标准排序相结合的列表,它将为每个排序提供O(m log m),执行m/n 次和O(n) 数据操作。结果是O(n + n log m)

那么,为什么要选择您可能会问的最简单的实现呢?答案很简单:简单的实现也很容易编译和优化,因为它们通常没有很多分支,不使用大量随机内存访问等。

最幼稚的实现(我已经在评论中建议过)是简单地将东西放入一个数组中,对其进行排序,然后删除它的下半部分。

基本上可以在最小的测试用例中这样实现:

static void Main(string[] args)
{
    Random rnd = new Random(12839);
    List<int> list = new List<int>();

    for (int i = 0; i < 5000; ++i)
    {
        list.Add(rnd.Next());
    }

    Stopwatch sw = Stopwatch.StartNew();
    for (int i = 0; i < 10000; ++i)
    {
        for (int j = 0; j < 5000; ++j)
        {
            list.Add(rnd.Next());
        }

        list.Sort((a, b) => a.CompareTo(b)); // #1
        list.RemoveRange(0, 5000);
    }

    Console.WriteLine(sw.ElapsedMilliseconds);
    Console.ReadLine();
}

基准性能:10047 毫秒。

优化一:移除方法调用和分支

方法调用需要时间。分支机构也是如此。因此,如果我们不需要分支,我们不妨将其消除。换句话说:这大约是 (5)。

想到的一件事是将#1替换为:

list.Sort((a, b) => a - b);

在大多数(!)场景中,这给出了预期的结果,我直言不讳地假设这种场景也不例外。

测量:8768 毫秒。(是的,人,这是 15% 的变化!)

为了好玩,我们还对 (2) 做了一个简单的测试:

list.Sort((a, b) => (int)((float)a - (float)b));

它与运算符的大小完全相同(32 位),它是完全相同的数据,并且会给出相同的结果——我们只是比较了所有具有不同 CPU 操作的东西并添加了一些强制转换。测量:10902 毫秒。如果每个操作都只是一个时钟滴答,这将超出您的预期。

优化2:数组还是列表?

我也可以关心列表本身;我们对它使用了很多调用,所以我们可以用它代替一个数组。如果我们反转排序顺序,我们甚至可以消除RemoveRange。那么我为什么不专注于这个呢?好吧,实际上我可以,但我可以告诉你这不会有太大的不同,因为相对而言,它并不经常被调用。不过,测试没有坏处,对吧?:

static void Main(string[] args)
{
    Random rnd = new Random(12839);

    int[] list = new int[10000];

    for (int i = 0; i < 5000; ++i)
    {
        list[i] = rnd.Next();
    }

    Stopwatch sw = Stopwatch.StartNew();
    for (int i = 0; i < 10000; ++i)
    {
        for (int j = 0; j < 5000; ++j)
        {
            list[j + 5000] = rnd.Next();
        }
        Array.Sort(list, (a, b) => b - a);
    }

    Console.WriteLine(sw.ElapsedMilliseconds);

    Console.ReadLine();
}

现在,这里有两个测量值:

  • 将列表更改为数组只是将其更改为 +/- 8700 毫秒 - 没有太大区别
  • 颠倒顺序将结果更改为 7456 毫秒。

这并没有真正产生影响的原因是List 的底层数据结构是一个数组,所以如果我们正在排序,我们只是在做同样的事情。这就是我们的时间所在。

这里要记住的不是数组和List 一样快。事实是:我发现如果它们是,它们实际上在很多情况下更快。但是,在这种情况下,我们不是在讨论内部循环中的优化,我们不会过度分配过多的内存(可能所有内容都保存在缓存中)并且所有内存访问都是对齐的。总而言之,因此我们可以预期差异会非常小。

优化3:移除更多方法调用

现在,您应该注意到这里还有一个替代方案:方法调用会花费时间,而这里调用最多的是比较器。所以让我们回到List 的解决方案并删除比较操作。当我们这样做时,我们仍然必须复制。你期待什么?

将行改为:

list.Sort();

...我们有一个新的时间:4123 毫秒。

现在,公平地说,实际上我们在这里所做的是将内联委托更改为Comparer&lt;int&gt;.Default,这是整数比较器的默认实现。委托将被包装在一个比较器中,创建 2 个虚拟调用 - 这只是 1 个调用。这意味着我们也可以通过实现我们自己的比较器类来反转顺序,这将是一个更快的解决方案。

优化4:Merge-join

如果我们只需要对一半的数据进行排序,为什么还要对所有内容进行排序?这没有道理,对吧?

再次,我选择最简单的算法来完成任务。我们按顺序遍历列表,并按顺序存储新项目,c.f. (1) 和 (3)。没有交换,请记住我们更喜欢顺序数据访问模式。然后,我们只需删除所有不再需要的东西。

我们需要的算法是一个合并连接,它的工作原理是这样的:

Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < 10000; ++i)
{
    for (int j = 0; j < 5000; ++j)
    {
        list.Add(rnd.Next());
    }

    // Sort the second half:
    list.Sort(5000, 5000, Comparer<int>.Default);
            
    // Both the lower and upper half are sorted. Merge-join:
    int lhs = 0;
    int rhs = 5000;
    while (list.Count < 15000)
    {
        int l = list[lhs];
        int r = list[rhs];
        if (l < r)
        {
            list.Add(l);
            ++lhs;
        }
        else if (l > r)
        {
            list.Add(r);
            ++rhs;
        }
        else
        {
            while (list.Count < 15000 && list[lhs] == l)
            {
                list.Add(l);
                ++lhs;
            }
            while (list.Count < 15000 && list[rhs] == r)
            {
                list.Add(r);
                ++rhs;
            }
        }
    }

    list.RemoveRange(0, 10000);
}

我们有一个新的测量值,它是 3563 毫秒。

优化 5:RemoveRange #2

请记住,突发处理数据非常快。最后一段代码是展示这一点的绝佳机会。我们在这里使用RemoveRange,它以突发方式处理数据。我们还可以使用两个缓冲区并交换它们。基本上,我们在合并连接期间写入第二个list2,并将RemoveRange 替换为:

list.Clear();

var tmp = list;
list = list2;
list2 = tmp;

我们现在有了一个新的计时:3542 毫秒。完全一样!

从最后两个您应该得出结论,执行突发操作花费的时间非常少,您通常甚至不应该费心。

结论

我从一棵在 86220 毫秒内执行所有操作的树开始,最终得到一个耗时 3542 毫秒的算法。直截了当,这意味着最后一个实现在第一次尝试的 4% 的时间内执行。

现在,我在这个冗长的答案中确实使用了不同的算法 - 但重点是向您展示如何进行所有这些优化以及优化的真正效果。

【讨论】:

  • 在阅读之前我并不知道,但这正是我正在寻找的答案。我对如此小的变化如何对性能产生如此大的影响感到震惊。感谢您提供这个非常翔实的答案!
  • 我认为您犯了一个小错误:在合并连接中,您实际上是在丢弃上半部分而不是下半部分,不是吗?
  • @Safron 是的,你是对的。这不是生产级;通常我会先用断言创建一个“测试”,然后开始优化——这可以确保所有中间结果都是正确的。这里我主要关注主要理论,遗漏了很多东西,很少考虑实际结果的正确性……要解释的东西太多了,也许我应该写一本书。 :)
【解决方案2】:

我假设您希望始终保持列表排序。用随机整数替换下半部分的最佳方法是:

  1. 移除下半部分。
  2. 添加随机整数以恢复原始大小
  3. 对列表进行排序

之前,您插入到了正确的位置。这有效地实现了非常慢的选择排序。相反,让内置的高效排序算法完成繁重的工作。

这应该是O(n * log n)。以前是O(n^2)

您可以通过不删除前半部分来优化这个常数。而是将其替换为随机数,然后排序。

【讨论】:

  • 这是我第一次阅读问题时想到的确切算法,尽管在 OP 提供更多信息之前它是推测性的。
  • 我以为我之前的算法只有O(n)而不是O(n^2)。这是为什么?似乎就像现在每个人都建议的那样,在添加所有项目后进行排序可能是可行的方法。
  • 每次插入需要 N 单位时间,您插入 N 次。那是 N^2 和插入排序(不是答案中错误说明的选择)。
【解决方案3】:

为什么假设您需要不同的数据结构?这么说:

我做的第一件事就是使用一个列表,通过在正确的位置添加新项目来保持排序。虽然删除排序列表的下半部分很容易,但插入需要相当多的时间

担心我,因为您可能使用了正确的 [1] 数据结构,但算法很差。我可以强烈建议您查看http://sscce.org/ 并将其包含在您的问题中吗?

但列表插入速度慢 O(n)!

不要插入!

正如@usr 所解释的,一个更好的算法可能是这样的:

  1. 移除下半部分。
  2. 添加随机整数以恢复原始大小
  3. 对列表进行排序

无需更改数据结构,但对解决问题的方式有很大的改变。

这一点尤其重要,因为正如@atlaste 重申的那样,并非每个系统都是平等的,无论 O(?):

现代处理器不再那么容易了。我见过 相同算法的不同实现为您提供的情况 由于分支预测、矢量化和 缓存位置。如果您进行比较,O(..) 计算非常好 苹果和苹果——但不幸的是,情况可能并非如此。我 希望大家只看这个视频: youtube.com/watch?v=GPpD4BBtA1Y

但我仍然是 O(log n) 而不是 O(n) 数据结构!

好的,在我们结束并开始实际概述您使用的算法和衡量性能之前(目前这似乎太多了),让我问您一个问题:

假设我们有一个“大整数排序列表(>1000 项)”。事实上,假设这个列表有 10,000 个条目!

在 O(?) 方面,其中哪一个具有更好的插入性能?

A) 列表

B) 链表

C) 二叉树

当你准备好了,看看答案:

他们都有 O(1)! O(n) 仅告诉您事物 缩放 的程度(相对于它们自身,并且仅在广义上)。由于该列表的大小固定为“10,000 个项目”,因此没有缩放(所有内容都被视为“恒定因素”)。请注意,我并不是说这些结构具有同样的性能......只是 O(?) 在其描述中存在限制。欲了解更多信息What is a plain English explanation of "Big O" notation?

基准测试

这是插入排序的基准,与添加所有新随机项后的排序:http://pastebin.com/pNgx73cs

结果(默认设置)

Testing performance of filling a list to 10000 items 1000 times, discarding 1/2
of items after every fill!

Old list: 3248ms
New list: 547ms
DONE

请注意,即使我们在 O(?) 方面有更高效的方法,但结果相差并不大,因为在这种大小下,CPU 速度惊人!

注意事项:

  1. OP 具有相对较小的[2] 整数集合,应该很容易适应 CPU 缓存 [3] 甚至 L1 缓存。在这些情况下,小的、连续的和可预测的内存使用(例如数组和列表(在 C# 中基于数组))可能非常有效。
  2. 假设上限为 10,000,对于 32 位和 64 位系统,这将分别仅为 ~40KB 或 ~80KB。
  3. Intel Skylake 每个内核有 64 KiB 的 L1 缓存和 256KiB 的 L2 缓存:https://en.wikipedia.org/wiki/Skylake_(microarchitecture)

【讨论】:

  • 使用列表的问题是,当插入一个项目时,所有下一个项目都必须向下移动一个位置,这对于大列表来说可能需要很多时间,因为它是一个 O(n ) 算法。使用像二叉搜索树这样的另一种数据结构,这可以通过 O(log n) 算法来完成,当我们谈论大列表时,这要快得多。我稍后会举一个例子......
  • @Safron 不要将 O(n) 和 O(log n) 与性能混淆。不要将数据结构问题与算法问题混为一谈——在列表中插入通常很“慢”……那你为什么要插入呢?
  • 首先,我插入列表是因为我希望列表保持排序,因为这允许我通过调用RemoveRange(size/2,size/2) 删除下半部分(我按降序排序)。其次,如果我没记错的话,O(n) 和 O(log n) 显示了当列表/树的大小增长时性能会以多快的速度恶化,这在这种情况下尤其重要,因为它的大小很大。当您比较大小为 100 和大小为 10000 时,O(n) 算法的性能会差 100 倍,而 O(log2 n) 算法的性能只会差 2 倍。
  • @Safron 是也不是。有了现代处理器,事情就不再那么容易了。由于分支预测、向量化和缓存局部性,我见过相同算法的不同实现给你带来 100+ 的差异。如果您将苹果与苹果进行比较,O(..) 计算非常有用 - 但不幸的是,情况可能并非如此。我希望每个人都只看这个视频:youtube.com/watch?v=GPpD4BBtA1Y。话虽如此,我确实相信在这种特殊情况下,底线是你是对的(主要是因为 C# 不做积极的选择)。
  • @Atlaste 不要低估 C# - 一个简单的数组或列表的性能相当不错,尤其是在考虑使用分布在堆上的指针的其他数据结构时。数字,而不是猜测应该推动优化。
【解决方案4】:

@atlaste 的帖子信息量很大,但不管怎样,能不能做得更快一点?我稍微改变了实现,从 3750 毫秒到 3350 毫秒,这就是我的起点。如果你看一下算法,随着时间的推移,你会用随机数填充数组的一半,但很有可能你会使用很少的随机数。您可以立即丢弃所有大于前半部分最大数字的数字,而无需对它们进行排序。它将是大部分新数据,因此加速将是巨大的(对于随机输入)。应用这个想法,我得到了 640 毫秒。鉴于这一事实,470 毫秒生成随机数,是处理算法的 17 倍加速。但它可能会有所不同,具体取决于数据的特征。

和代码

public static List<int> orig()
{
    Random rnd = new Random(12839);

    List<int> list = new List<int>(10000);

    for (int i = 0; i < 5000; ++i)
    {
        list.Add(rnd.Next());
    }

    for (int i = 0; i < 10000; ++i)
    {
        for (int j = 0; j < 5000; ++j)
        {
            list.Add(rnd.Next());
        }

        // Sort the second half:
        list.Sort(5000, 5000, Comparer<int>.Default);

        // Both the lower and upper half are sorted. Merge-join:
        int lhs = 0;
        int rhs = 5000;
        while (list.Count < 15000)
        {
            int l = list[lhs];
            int r = list[rhs];
            if (l < r)
            {
                list.Add(l);
                ++lhs;
            }
            else if (l > r)
            {
                list.Add(r);
                ++rhs;
            }
            else
            {
                while (list.Count < 15000 && list[lhs] == l)
                {
                    list.Add(l);
                    ++lhs;
                }
                while (list.Count < 15000 && list[rhs] == r)
                {
                    list.Add(r);
                    ++rhs;
                }
            }
        }

        list.RemoveRange(0, 10000);
    }

    return list;
}


public static int[] altered()
{
    Random rnd = new Random(12839);

    int HALFSIZE = 5000;
    int SIZE = 2 * HALFSIZE;
    int TESTLOOPS = 10000;

    int[] list = new int[SIZE];
    int[] list2 = new int[SIZE];

    for (int i = 0; i < HALFSIZE; ++i)
    {
        list[i] = rnd.Next();
    }

    for (int i = 0; i < TESTLOOPS; ++i)
    {
        for (int j = HALFSIZE; j < list.Length; ++j)
        {
            list[j] = rnd.Next();
        }

        // Sort the second half:
        Array.Sort(list, HALFSIZE, HALFSIZE, Comparer<int>.Default);

        // Both the lower and upper half are sorted. Merge-join:
        int lhs = 0;
        int rhs = HALFSIZE;
        int i2 = 0;
        while (i2 < HALFSIZE)
        {
            int l = list[lhs];
            int r = list[rhs];
            if (l <= r)
            {
                list2[i2++] = l;
                ++lhs;
            }
            if (l >= r)
            {
                list2[i2++] = r;
                ++rhs;
            }
        }

        var tmp = list;
        list = list2;
        list2 = tmp;
    }

    return list;
}

public static int[] altered2()
{
    Random rnd = new Random(12839);

    int HALFSIZE = 5000;
    int SIZE = 2 * HALFSIZE;
    int TESTLOOPS = 10000;

    int[] list = new int[SIZE];
    int[] list2 = new int[SIZE];

    for (int i = 0; i < HALFSIZE; ++i)
    {
        list[i] = rnd.Next();
    }

    for (int i = 0; i < TESTLOOPS; ++i)
    {
        for (int j = HALFSIZE; j < list.Length; ++j)
        {
            list[j] = rnd.Next();
        }

        // quicksort one level to skip values >= maxValue
        int maxValue = list[HALFSIZE - 1];
        int ll = HALFSIZE;
        int rr = SIZE - 1;
        do
        {
            while (ll <= rr && list[ll] < maxValue) { ++ll; }
            while (ll < rr && list[rr] >= maxValue) { --rr; }
            if (ll < rr)
            {
                int swap = list[ll];
                list[ll] = list[rr];
                list[rr] = swap;
                ++ll;
                --rr;
            }
        }
        while (ll < rr);

        // Sort the second half:
        Array.Sort(list, HALFSIZE, ll - HALFSIZE, Comparer<int>.Default);

        // Both the lower and upper half are sorted. Merge-join:
        int lhs = 0;
        int rhs = HALFSIZE;
        int i2 = 0;
        while (i2 < HALFSIZE)
        {
            int l = list[lhs];
            int r = list[rhs];
            if (l <= r)
            {
                list2[i2++] = l;
                ++lhs;
            }
            if (l >= r)
            {
                list2[i2++] = r;
                ++rhs;
            }
        }

        var tmp = list;
        list = list2;
        list2 = tmp;
    }

    return list;
}

public static int[] random()
{
    Random rnd = new Random(12839);

    int HALFSIZE = 5000;
    int SIZE = 2 * HALFSIZE;
    int TESTLOOPS = 10000;

    int[] list = new int[SIZE];
    for (int i = 0; i < HALFSIZE; ++i)
    {
        list[i] = rnd.Next();
    }

    for (int i = 0; i < TESTLOOPS; ++i)
    {
        for (int j = HALFSIZE; j < list.Length; ++j)
        {
            list[j] = rnd.Next();
        }              
    }

    return list;
}

static void Main(string[] args)
{
    int HALFSIZE = 5000;
    var origTest = orig();                       
    Stopwatch sw = Stopwatch.StartNew();
    orig();
    sw.Stop();
    Console.WriteLine("Orig time: " + sw.ElapsedMilliseconds);

    var alteredTest = altered();
    sw = Stopwatch.StartNew();
    altered();
    sw.Stop();
    Console.WriteLine("Altered time: " + sw.ElapsedMilliseconds);
    Console.WriteLine("Test: " + (origTest.Take(HALFSIZE).SequenceEqual(alteredTest.Take(HALFSIZE)) ? "OK" : "BAD"));

    var altered2Test = altered2();
    sw = Stopwatch.StartNew();
    altered2();
    sw.Stop();
    Console.WriteLine("Altered2 time: " + sw.ElapsedMilliseconds);
    Console.WriteLine("Test: " + (origTest.Take(HALFSIZE).SequenceEqual(altered2Test.Take(HALFSIZE)) ? "OK" : "BAD"));

    sw = Stopwatch.StartNew();
    random();
    sw.Stop();
    Console.WriteLine("Just random time: " + sw.ElapsedMilliseconds);
    
    Console.ReadKey();
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-03-05
    • 2022-06-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-23
    • 2020-12-11
    相关资源
    最近更新 更多