【问题标题】:Most efficient way to sort an array and keep corresponding original indices对数组进行排序并保留相应原始索引的最有效方法
【发布时间】:2014-08-13 08:30:25
【问题描述】:

我想在 C# 中对一个整数数组进行排序,但还要保留数组中每个元素对应的原始索引。

我的第一个想法是转换为以键为索引、值为值的Dictionary对象;然后使用 linq 按值排序。我不认为这表现得很好。还有哪些可能的解决方案?性能是关键。

This 似乎是一个不错且简单的解决方案;但这是最快的方法吗?

【问题讨论】:

  • “这是最快的方法吗?” - 取决于您的数据和机器。 Measure it.

标签: c# arrays sorting indices


【解决方案1】:

如果您及时谈论性能,您可以将数组复制到第二个数组中,对第二个数组进行排序,然后使用两个数组来实现单独的功能。这将使您O(1) 可以访问所需的元素。

如果您在空间方面谈论性能,使用字典的方法是最好的,因为它只会保留 1 个元素的副本,从而导致 O(n) 空间。

像往常一样,在真正遇到性能问题之前不要进行优化。

【讨论】:

  • 我说的是时间性能,以及排序算法的效率,而不是访问元素。
  • @ArmenSafieh-Garabedian 排序算法将在 O(n*log(n)) 中执行 - 在一般情况下,没有什么可以做得更快。所以 Array.Sort 或 Linq 将以相同的速度执行,数组可能会快一点。
【解决方案2】:

您可以创建一个 KeyValuePairs 数组,然后按值排序:

Array.Sort(array, (left, right) => left.Value.CompareTo(right.Value))

但是 Array.Sort(Array, Array) 看起来也不错。

【讨论】:

    【解决方案3】:

    .NET 中有一组特定的内置函数可以执行此操作。寻找带有TKey[] 参数的Array.Sort 的重载。有几个重载可让您指定要排序的子范围,或自定义IComparer<TKey>。秘诀是将原始数组作为keys 参数传递,并为items 参数传递一个身份数组(0, 1, 2,... n-1)。以下函数将为您完成所有工作:

    /// sort array 'rg', returning the original index positions
    static int[] SortAndIndex<T>(T[] rg)
    {
        int i, c = rg.Length;
        var keys = new int[c];
        if (c > 1)
        {
            for (i = 0; i < c; i++)
                keys[i] = i;
    
            System.Array.Sort(rg, keys /*, ... */);
        }
        return keys;
    }
    

    同样,对于Array.Sort,请注意我们要小心可能令人困惑的参数名称。我们将 items 作为第一个参数(称为“keys”)传入,而我们的 index-to-be(感觉更像键)作为第二个参数(称为“items”)。

    用法不言自明:

    var rgs = new[] { "xyz", "a", "", "bb", "pdq" };
    
    int[] idx = SortAndIndex(rgs);  // rgs: { "",  "a", "bb", "pdz", "xyz" }
                                    // idx: {  2,   1,    3,    4,     0   }
    

    这涵盖了 OP 的情况,您实际上希望原始数据最终排序。如果这是您需要的,您可以在此处停止阅读。

    但是一个相关的问题是,如果你想要那些相同的排序指标,但你不想修改原始数组怎么办?我们如何获得排序索引而不改变原始项目的顺序?

    我发现做到这一点的最佳方法实际上是使用上述过程对数据进行排序并获取索引,然后使用该排序索引将已排序的项目恢复为原始订单

    可能有几种方法可以做到这一点,但由于这个问题提到了效率,我可以展示一些保证执行最少数量的原始项目交换的代码,同时只使用一个 T 存储元素,为了将项目恢复到原始的未排序顺序:

    static unsafe void RevertSortIndex<T>(T[] rg, int[] keys)
    {
        int i, k, c;
        int* rev = stackalloc int[c = rg.Length];
        for (i = 0; i < c; i++)
            rev[k = keys[i]] = k != i ? i : -1;
    
        do
            if ((i = rev[--c]) != c && i >= 0)
            {
                T t = rg[k = c];
                do
                {
                    rg[k] = rg[i];
                    rev[k] = -1;
                }
                while ((i = rev[k = i]) != c);
    
                rg[k] = t;
                rev[k] = -1;
            }
        while (c > 0);
    }
    

    为了只使用单个T 元素进行交换,并且每个元素仅移动一次到其最终位置,您必须按照数据确定的非常特定的顺序进行交换。临时反向索引 (rev) 简化了这一点,该索引很容易从 keys 创建。这里显示为stackalloc,但如果您不想走这条路,您可以轻松地将其替换为托管的int[] 分配。

    无需过多详细介绍,任何排序索引都包含一个或多个从一个链接到另一个的项目“链”,并且遵循每个链为您提供了一个最佳顺序,您可以将这些元素恢复到其原始位置,同时只保留一个临时的T。这就是内部 do...while 循环的作用。

    需要外部while...循环来扫描额外的链,因为排序索引作为一个整体可能有多个独立的链,它们都需要遵循。重要的是,为了得到正确的结果,每条链必须只处理一次,不能再处理。因此,为了查明任何给定的交换是否已经被处理,它在rev 临时反向索引中的条目被设置为-1。这表明rg 中对应的T 元素已被移动(作为先前链的一部分)。

    这是完整的用法示例:

    var rgs = new[] { "xyz", "a", "", "bb", "pdq" };
    
    int[] idx = SortAndIndex(rgs);
    
    // rgs: { "",  "a", "bb", "pdz", "xyz" }
    // idx: {  2,   1,    3,    4,     0   }
    
    RevertSortIndex(rgs, idx);
    
    // rgs: { "xyz", "a", "", "bb", "pdq"  }
    // idx: {   2,   1,    3,    4,     0  }    (unchanged)
    

    最后一点是SortAndIndexRevertSortIndex 的组合可能会给出rgs 未修改的外观,但这不应依赖于并发目的。如果rgs 同时从其他地方可见,则中间状态将可见。

    【讨论】:

      【解决方案4】:

      虽然老式且未键入 Array.Sort(Array keys, Array items),但它在跟踪索引方面比 LINQ 更好。

      进入数组实现:

      • C# Array 的 Github 源代码
      • CPP平台实现部分
      • Matt Warren - 如果你真的想了解数组

      Array.Sort 与 Linq

          [GlobalSetup]
          public virtual void Setup()
          {
              data = new T[N];
              indexes = new int[N];
              for (var cc = 0; cc < N; cc++)
              {
                  data[cc] = GetRandom();
                  indexes[cc] = cc;
              }
          }
      
          // Clone is nessesary as Array.Sort is done in place, ie the next call will be incorrectly given a pre-sorted list
          private T[] GetTestData() => (T[]) data.Clone();
          private int[] GetTestDataIndex() => (int[])indexes.Clone();
      
          [Benchmark]
          public virtual void Sort()
          {
              Array.Sort(GetTestData());
          }
      
          [Benchmark]
          public virtual void SortMaintainIndex()
          {
              Array.Sort(GetTestData(), GetTestDataIndex());
          }
      
          [Benchmark]
          public virtual void SortWithLinq()
          {
              int cc = 0;
              var withIndex = GetTestData()
                        .Select(x => (cc++, x))
                        .OrderBy(x => x.x)
                        .ToArray();
          }
      

      在速度方面没有可比性: 来源这里https://gist.github.com/guylangston/cd9a0719d467f020eba46c6d0beb0584

      BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
      Intel Core i7-3930K CPU 3.20GHz (Ivy Bridge), 1 CPU, 12 logical and 6 physical cores
      .NET Core SDK=2.1.300
        [Host]     : .NET Core 2.1.0 (CoreCLR 4.6.26515.07, CoreFX 4.6.26515.06), 64bit RyuJIT
        DefaultJob : .NET Core 2.1.0 (CoreCLR 4.6.26515.07, CoreFX 4.6.26515.06), 64bit RyuJIT
      
      
                  Method |     N |        Mean |      Error |     StdDev |      Median |
      ------------------ |------ |------------:|-----------:|-----------:|------------:|
                    Sort |  1000 |    35.85 us |  0.3234 us |  0.2700 us |    35.76 us |
       SortMaintainIndex |  1000 |    60.82 us |  0.2280 us |  0.1780 us |    60.76 us |
            SortWithLinq |  1000 |   172.26 us |  3.3984 us |  3.7773 us |   170.75 us |
                    Sort | 10000 |   611.82 us | 13.8881 us | 18.0584 us |   602.77 us |
       SortMaintainIndex | 10000 |   889.25 us | 18.6503 us | 28.4810 us |   874.06 us |
            SortWithLinq | 10000 | 2,484.35 us | 57.8378 us | 54.1015 us | 2,476.72 us |
      

      【讨论】:

      • 关于公共静态 void Array.Sort (Array keys, Array items) 上的参数名称的地方我发现很棘手;从调用者的角度来看,这似乎是错误的方式
      猜你喜欢
      • 2017-07-12
      • 1970-01-01
      • 1970-01-01
      • 2017-04-28
      • 1970-01-01
      • 1970-01-01
      • 2019-10-19
      • 2011-11-04
      • 2021-09-20
      相关资源
      最近更新 更多