使用LINQ获取下一个可用的整数

时间:2012-06-19 01:44:51

标签: c# linq linq-to-objects

说我有一个整数列表:

List<int> myInts = new List<int>() {1,2,3,5,8,13,21};

我想获得下一个可用的整数,按增加整数排序。不是最后一个或最高的,但在这种情况下,下一个不在此列表中的整数。在这种情况下,数字是4。

是否有LINQ语句会给我这个?如:

var nextAvailable = myInts.SomeCoolLinqMethod();

编辑:废话。我说答案应该是2,但我的意思是4.我为此道歉!

例如:想象一下,您负责分发流程ID。您想获取当前进程ID的列表,并发出下一个进程ID,但下一个进程ID不应该只是最高值加1。相反,它应该是从有序的进程ID列表中可用的下一个。你可以从最高的开始获得下一个可用的,这并不重要。

7 个答案:

答案 0 :(得分:25)

我看到很多编写自定义扩展方法的答案,但是可以使用标准的linq扩展方法和静态Enumerable类来解决这个问题:

List<int> myInts = new List<int>() {1,2,3,5,8,13,21};

// This will set firstAvailable to 4.
int firstAvailable = Enumerable.Range(1, Int32.MaxValue).Except(myInts).First();

答案 1 :(得分:3)

@Kevin提供的答案有不良的表现形象。逻辑将多次访问源序列:一次用于.Count呼叫,一次用于.FirstOrDefault呼叫,一次用于每个.Contains呼叫。如果IEnumerable<int>实例是延迟序列,例如.Select调用的结果,则这将导致序列的至少2次计算,以及每个数字的一​​次计算。即使您将列表传递给方法,它也可能会遍历每个选中号码的整个列表。想象一下,在序列{ 1, 1000000 }上运行它,你可以看到它不会表现得如何。

LINQ努力迭代源序列不超过一次。这通常是可能的,并且会对代码的性能产生很大影响。下面是一个扩展方法,它将迭代序列一次。它是通过查找每个连续对之间的差异来实现的,然后将第一个较低的数字加1,这比下一个数字多1:

public static int? FirstMissing(this IEnumerable<int> numbers)
{
    int? priorNumber = null;

    foreach(var number in numbers.OrderBy(n => n))
    {
        var difference = number - priorNumber;

        if(difference != null && difference > 1)
        {
            return priorNumber + 1;
        }

        priorNumber = number;
    }

    return priorNumber == null ? (int?) null : priorNumber + 1;
}

由于可以在任意整数序列上调用此扩展方法,因此我们确保在迭代之前对它们进行排序。然后我们计算当前数字和前一个数字之间的差异。如果这是列表中的第一个数字,priorNumber将为空,因此difference将为空。如果这不是列表中的第一个数字,我们会检查与前一个数字的差异是否正好为1.如果不是,我们知道存在差距,我们可以将前一个数字加1。

您可以根据需要调整return语句以处理包含0或1项的序列;我选择为空序列返回null,为序列{ n }返回n + 1。

答案 2 :(得分:2)

这将非常有效:

static int Next(this IEnumerable<int> source)
{
    int? last = null;
    foreach (var next in source.OrderBy(_ => _))
    {
        if (last.HasValue && last.Value + 1 != next)
        {
            return last.Value + 1;
        }

        last = next;
    }

    return last.HasValue ? last.Value + 1 : Int32.MaxValue;
}

答案 3 :(得分:0)

public static class IntExtensions
{
    public static int? SomeCoolLinqMethod(this IEnumerable<int> ints)
    {
        int counter = ints.Count() > 0 ? ints.First() : -1;

        while (counter < int.MaxValue)
        {
            if (!ints.Contains(++counter)) return counter;
        }

        return null;
    }
}

用法:

var nextAvailable = myInts.SomeCoolLinqMethod();

答案 4 :(得分:0)

好的,我提出的解决方案对我有用。

var nextAvailableInteger = Enumerable.Range(myInts.Min(),myInts.Max()).FirstOrDefault( r=> !myInts.Contains(r));

如果有人有更优雅的解决方案,我很乐意接受这一点。但就目前而言,这正是我在我的代码中所做的并继续前进。

编辑:这是我在Kevin建议添加扩展方法后实现的。这才是真正的答案 - 没有一个LINQ扩展会这样做,所以添加我自己的更有意义。这就是我真正想要的。

public static int NextAvailableInteger(this IEnumerable<int> ints)
{
    return NextAvailableInteger(ints, 1); // by default we use one
}
public static int NextAvailableInteger(this IEnumerable<int> ints, int defaultValue)
{
    if (ints == null || ints.Count() == 0) return defaultValue;

    var ordered = ints.OrderBy(v => v);
    int counter = ints.Min();
    int max = ints.Max();

    while (counter < max)
    {
        if (!ordered.Contains(++counter)) return counter;
    }
    return (++counter);
}

答案 5 :(得分:0)

不确定这是否符合酷Linq方法,但使用This SO Answer

中的左外连接理念
var thelist = new List<int> {1,2,3,4,5,100,101};

var nextAvailable = (from curr in thelist
                    join next in thelist
                        on curr + 1 equals next into g
                    from newlist in g.DefaultIfEmpty()
                    where !g.Any ()
                    orderby curr
                    select curr + 1).First();

如果您使用Linq to Sql,这会将处理放在sql server端,并且您不必将ID列表从服务器拉到内存。

答案 6 :(得分:0)

var nextAvailable = myInts.Prepend(0).TakeWhile((x,i) => x == i).Last() + 1;

已经7年了,但是比选择的答案或获得最高票数的答案更好的方法。

该列表已经按顺序排列,并且根据示例0不计算在内。我们可以在0之前加上前缀,然后检查每个项目是否与其索引匹配。当TakeWhile遇到不匹配的数字或在列表的末尾时,它将停止评估。
答案是匹配的最后一项,再加上1。
TakeWhile比枚举所有可能的数字然后使用Except排除现有数字更有效,因为我们的TakeWhile只会遍历列表,直到找到第一个可用数字,并且得到的Enumerable集合最多为n。
使用Except的答案会生成一个完整的,可枚举的答案,而这些答案并不仅仅是抓住第一个答案。 Linq可以使用First()进行一些优化,但与TakeWhile相比,它仍然慢得多,并且占用的内存也更多。