使用 LINQ 排序后获取集合中项目的新索引

Posted

技术标签:

【中文标题】使用 LINQ 排序后获取集合中项目的新索引【英文标题】:Get new indices of items in a collection after sorting using LINQ 【发布时间】:2016-06-06 19:21:16 【问题描述】:

我想在 C# 中对列表(或数组)进行排序,并希望为未排序列表中的每个项目保存新索引。即:

A = 2 3 1

sorted(A) = 1 2 3

indexes = 1 2 0 <-- This is what I need

我在 Stack Overflow 上读到 LINQ 特别有用,但我找不到具体的方法。

【问题讨论】:

How to sort based on ordering的可能重复 索引不应该是2 0 1? 您是否尝试过结合使用 foreach 和 .indexOf() 进行迭代? 为了记录,这通常称为排名函数。 2 排名为 1,3 排名为 2,1 排名为 0 【参考方案1】:

注意:

我犯了一个严重错误,误读了 OP 的问题(很尴尬,这得到了如此多的支持)。我希望(理想情况下)OP 接受其他答案之一为正确的。为了公平对待支持者并本着 SO 的精神,我将修改此答案以使其正确。


一个可以携带两种扩展方法,一种用于旧索引,一种用于新索引(这是 OP 想要的)。

public static IEnumerable<int> OldIndicesIfSorted<T>(this IEnumerable<T> source) where T : IComparable<T>

    return source
        .Select((item, index) => new  item, index )
        .OrderBy(a => a.item)
        .Select(a => a.index);


public static IEnumerable<int> NewIndicesIfSorted<T>(this IEnumerable<T> source) where T : IComparable<T>

    return source
        .OldIndicesIfSorted()
        .Select((oldIndex, index) => new  oldIndex, index )
        .OrderBy(a => a.oldIndex)
        .Select(a => a.index);

这样称呼,

//A = [2, 3, 1];
A.NewIndicesIfSorted(); // should print [1, 2, 0]

这两种方法都是懒惰的,但NewIndicesIfSorted 最好按照Matthew Watson 的答案编写,这样效率更高。我和 Mat 的答案的优点是可以很好地处理重复条目。

【讨论】:

哦,我可能做到了。 :) A 而不是 a。是的,它工作正常。对不起! 但是 OP 不想要 [1, 2, 0] (...为每个项目保存 新索引..." i>) 还是不明白3的索引应该是0o.O @SonerGönül 索引显示原始项目在排序数组中的最终位置。因此,索引 0 处的原始项目(即值 2)最终位于索引 1。索引 1 处的原始项目(即值 3)最终位于索引 2。索引 2 处的原始项目(即值1) 以索引 0 结束。因此:1, 2, 0 具有讽刺意味的是,OrderBy 在内部通过生成一组重新排序的索引,然后枚举它们来工作。 (一方面,交换两个索引比交换两个大值类型元素更便宜)。【参考方案2】:

您可以使用 LINQ 选择来做到这一点:

var a =new [] 2,3,1;

var sorted = a.OrderBy (x => x).ToList();
var indexes = a.Select (x => sorted.IndexOf(x));

如果这是一个巨大的列表,而不是这个简单的列表,它可能会有点低效,但确实会返回您所期望的。

【讨论】:

@downvoter:你能描述一下这个答案有什么问题吗?因为在我看来,它是正确的。它根据 OP 的要求产生正确的答案 1 2 0。 @MatthewWatson 一定是我在第 1 行的 equals 后面没有空格。我去坐在角落里想想我现在做了什么:( 我不是@downvoter,但我猜这是因为IndexOf 解决方案与Select((item, index) =&gt; ... 解决方案相比效率相对较低。 @andrew.cance 我没有投反对票,但为了提醒大家注意,如果出现重复,您的方法可能无法按预期工作。【参考方案3】:

一种方法如下:

int[] a = 2, 3, 1;
int[] b = Enumerable.Range(0, a.Length).ToArray();
int[] c = new int[a.Length];

Array.Sort(a, b);

for (int i = 0; i < b.Length; ++i)
    c[b[i]] = i;

Console.WriteLine(string.Join(", ", c)); // Prints 1, 2, 0

它使用Array.Sort() 的重载,它使用来自不同数组的值对一个数组进行排序。

我认为这是相当有效的:它使用 O(N log(N)) 排序和 O(N) 重新映射。 (其他正确的解是O(N log(N))加上O(N^2))

不过,它只适用于数组(因为 List 没有与 Array.Sort() 等效的东西)。

使用 Linq 的替代方法(实际上是对 @nawfal 答案的修正):

int[] a = 2, 3, 1;

var b = a
    .Select((item, index) => new  item, index )
    .OrderBy(x => x.item)
    .Select(x => x.index)
    .ToArray();

int[] c = new int[b.Length];

for (int i = 0; i < b.Length; ++i)
    c[b[i]] = i;

Console.WriteLine(string.Join(", ", c)); // Prints 1, 2, 0

这里的关键是两种方法中的额外映射步骤:

int[] c = new int[b.Length];

for (int i = 0; i < b.Length; ++i)
    c[b[i]] = i;

这实际上是一个反向查找映射。

【讨论】:

有趣的方式来做到这一点,但使用 Enumerable.Range 似乎并不是最清晰和最直接的实现方式 @AFract 好吧,您只需要一个索引列表来与键一起排序,Enumerable.Range() 几乎是我所知道的最简单的生成方法。 @downvoter:你能描述一下这个答案有什么问题吗?因为在我看来,它是正确的。它会根据 OP 的要求生成正确的答案 1 2 0 (请注意,尽管我在上面发表了评论,但我不是反对者,我的评论不再相关,因为您已经编辑了答案) 我当然是支持者之一。真的很想知道为什么好的正确答案被低估了,同时微不足道且明显不正确的答案却被高度赞成。非常令人失望。【参考方案4】:

另一个可行的解决方案:

var input = new List<int>  2, 3, 1 ;
var result = input.Select(x => input.OrderBy(n => n).ToList().IndexOf(x));

Console.WriteLine(String.Join(" ", result));  // 1 2 0

【讨论】:

这与安德鲁的答案相同,显然性能较差。将 orderby 放在循环之外。【参考方案5】:

在阅读其他答案时,我对您到底想要哪个indexes 订单感到有些困惑,但如果您希望它们按照您在问题中发布的1 2 0 的顺序排列,这里有一个解决方案。试试这个:

var A = new int[]  2, 3, 1 ;
var indexes = new int [A.Length];

var sorted = A.OrderBy(n => n)
              .Select((n, i) => 
                       
                          indexes[Array.IndexOf(A, n)] = i;

                          return n; 
                      )
              .ToArray();

【讨论】:

如果您有重复的条目将不起作用。另请注意,像这样的 LINQ 查询中的副作用并不多。

以上是关于使用 LINQ 排序后获取集合中项目的新索引的主要内容,如果未能解决你的问题,请参考以下文章

简单介绍C#集合查询Linq在项目中使用详解

如何获取 Linq 查询结果集合中的索引?

根据另一个集合项目对项目进行排序

使用 C# 、 LINQ 进行排序

使用Linq获取列表中对象的索引[重复]

获取可绑定集合中项目的索引