使用 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
的索引应该是0
o.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) => ...
解决方案相比效率相对较低。
@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 排序后获取集合中项目的新索引的主要内容,如果未能解决你的问题,请参考以下文章