最有效的方法来排序数组并保留相应的原始索引

时间:2014-06-23 10:23:31

标签: c# arrays sorting indices

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

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

This似乎是一个很好的解决方案;但这是最快的方式吗?

4 个答案:

答案 0 :(得分:1)

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

如果你谈论空间方面的表现,你使用词典的方法是最好的,因为它只会保留1个副本的元素,从而产生O(n)空格。

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

答案 1 :(得分:1)

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

/// 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,请注意我们对可能令人困惑的参数名称非常谨慎。我们将作为第一个参数(称为“键”)传递,我们的索引要(感觉更像是键)作为传入第二个参数(称为“项目”)。

用法非常明显:

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。这表示T中相应的rg元素已被移动(作为上一个链的一部分)。

以下是完整的用法示例:

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,则可以看到临时状态。

答案 2 :(得分:1)

虽然老式和未打字Array.Sort(Array keys, Array items),但追踪索引比LINQ更好。

进入Array实现:

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

Array.Sort vs 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 |

答案 3 :(得分:0)

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

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

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