【问题标题】:Covert List<int> to a list of ranges将 List<int> 转换为范围列表
【发布时间】:2020-03-09 22:45:00
【问题描述】:

我需要将整数的list 转换为范围列表。

我有一个包含8, 22, 41List&lt;int&gt;。这些值是从147 的完整列表中的分节符

我正在尝试获取包含起始行和结束行的范围列表。输出应该是

{(1,7),(8,21),(22,40),(41,47)}

我已尝试从question调整解决方案,但无法使其正常工作。

看起来应该很简单,但也许不是。

【问题讨论】:

  • 忘记了 C# 的标签
  • 作为一般提示,如果我在这种情况下,我可能会创建一个名为 Section 的自定义类/结构,然后让实现此行为的方法返回 List&lt;Section&gt;Section[]而不是Tuple&lt;int,int&gt;[];元组数组方法将起作用,但您可以提供自定义类型,如 SectionStartEnd 字段/属性,它比 Item1Item2 等更具描述性,并且更具可读性(IMO ) 从长远来看。
  • 其实这就是我想要做的。一直在用垃圾代码结束。我会将元组概念整合到类中。
  • 如果你愿意,我可以用这样的设置写一些相当干净的东西。该类可以包含一个元组,但我个人更喜欢单独的、命名良好的属性/字段。 (我可能会创建readonly 字段)
  • 如果分节符包含 1 怎么办?如果它们包括 47 怎么办?如果它们包括 46 个呢?如果它们包含大于 47 或小于 1 的数字怎么办?

标签: c# list range


【解决方案1】:

即使程序有效,在查询期间您应该对本地进行变异的答案也是危险的;不要养成这个坏习惯。

解决问题的最简单方法是编写一个迭代器块。假设您有明显的Pair&lt;T&gt; 类型;在 C# 7 中,您可能会使用元组;调整它以使用元组是一个简单的练习:

static IEnumerable<Pair<int>> MakeIntervals(
  /* this */ IEnumerable<int> dividers, /* You might want an extension method.*/
  int start,
  int end)
{
  // Precondition: dividers is not null, but may be empty.
  // Precondition: dividers is sorted.  
  // If that's not true in your world, order it here.
  // Precondition: dividers contains no value equal to or less than start.
  // Precondition: dividers contains no value equal to or greater than end.
  // If it is possible for these preconditions to be violated then
  // the problem is underspecified; say what you want to happen in those cases.
  int currentStart = start;
  for (int divider in dividers) 
  {
    yield return new Pair<int>(currentStart, divider - 1);
    currentStart = divider;
  }
  yield return new Pair<int>(currentStart, end);
}

这是解决这个问题的正确方法。如果你想变得有点傻,你可以使用Zip。从两个有用的扩展方法开始:

static IEnumerable<T> Prepend<T>(this IEnumerable<T> items, T first)
{
  yield return first;
  foreach(T item in items) yield return item;
}
static IEnumerable<T> Append<T>(this IEnumerable<T> items, T last)
{
  foreach(T item in items) yield return item;
  yield return last;
}

现在我们有了:

static IEnumerable<Pair<int>> MakeIntervals(
  IEnumerable<int> dividers,
  int start,
  int end)
{
  var starts = dividers.Prepend(start);
  // This is the sequence 1, 8, 22, 41
  var ends = dividers.Select(d => d - 1).Append(end);
  // This is the sequence 7, 21, 40, 47
  var results = starts.Zip(ends, (s, e) => new Pair<int>(s, e));
  // Zip them up: (1, 7), (8, 21), (22, 40), (41, 47)
  return results;
}

但是与直接编写迭代器块相比,这似乎是不必要的巴洛克式。此外,这会迭代两次集合,这被许多人认为是不好的风格。

解决问题的一个可爱方法是推广第一个解决方案:

static IEnumerable<R> SelectPairs<T, R>(
  this IEnumerable<T> items,
  IEnumerable<T, T, R> selector
)
{
  bool first = true;
  T previous = default(T);
  foreach(T item in items) {
    if (first) {
      previous = item;
      first = false;
    }
    else
    {
      yield return selector(previous, item);
      previous = item;
    }
  }
}

现在你的方法是:

static IEnumerable<Pair<int>> MakeIntervals(
  IEnumerable<int> dividers,
  int start,
  int end)
{
  return dividers
    .Prepend(start)
    .Append(end + 1)
    .SelectPairs((s, e) => new Pair<int>(s, e - 1);
}

我很喜欢最后一张。也就是说,给定8, 22, 41,我们构造1, 8, 22, 41, 48,然后从中挑选出对并构造(1, 7), (8, 21),等等。

【讨论】:

  • @WouldRatherBuildAMotor 这个答案绝对是要走的路。
【解决方案2】:

它不是很干净,但是您可以构建一个新数组并循环来填补空白。这假设您的数字已排序,并且我使用了Tuple&lt;int, int&gt; 返回,因为我找不到任何有意义的简单范围类型。使用此代码,您不必担心将状态存储在循环变量之外的变量中,也无需排除任何结果。

public Tuple<int, int>[] GetRanges(int start, int end, params int[] input)
{
    // Create new array that includes a slot for the start and end number
    var combined = new int[input.Length + 2];
    // Add input at the first index to allow start number
    input.CopyTo(combined, 1);
    combined[0] = start;
    // Increment end to account for subtraction later
    combined[combined.Length - 1] = end + 1;

    // Create new array of length - 1 (think fence-post, |-|-|-|-|)
    Tuple<int, int>[] ranges = new Tuple<int, int>[combined.Length - 1];
    for (var i = 0; i < combined.Length - 1; i += 1) {
        // Create a range of the number and the next number minus one
        ranges[i] = new Tuple<int, int>(combined[i], combined[i+1] - 1);
    }

    return ranges;
}

用法

GetRanges(1, 47, 8, 22, 41); 

GetRanges(1, 47, new [] { 8, 22, 41 }); 

如果您想要一个替代的纯 linq 解决方案,您可以使用它,

public Tuple<int, int>[] GetRanges(int start, int end, params int[] input)
{       
    return input
        .Concat(new [] { start, end + 1 }) // Add first and last numbers, adding one to end to include it in the range
        .SelectMany(i => new [] { i, i - 1 }) // Generate "end" numbers for each start number
        .OrderBy(i => i)
        .Except(new [] {start - 1, end + 1}) // Exclude pre-first and post-last numbers
        .Select((Value, Index) => new { Value, Index }) // Gather information to bucket values
        .GroupBy(p => p.Index / 2) // Create value buckets
        .Select(g => new Tuple<int, int>(g.First().Value, g.Last().Value)) // Convert each bucket into a Tuple
        .ToArray();
}

【讨论】:

  • 返回 (1,7) (8,21) (22,40) (41,46) 最后一项,47,不包括在内。
  • 很抱歉。您可以在结尾编号中添加一个来说明这一点。我更新了我的答案。
  • 我接受了之前的 linq 答案,它是第一个并且有效。但是一些非常基本的测试表明您的方法要快得多 Linq: TotalMilliseconds: 4.0944 Array: TotalMilliseconds: 0.225
  • @WouldRatherBuildAMotor:这里的 Linq 解决方案设计过度;它可能会简单得多。
  • @EricLippert 很公平,这有点事后诸葛亮,因为我不喜欢 Linq 的其他答案。听起来他们喜欢我的非 linq 答案。看起来你的回答涵盖了我的担忧。
【解决方案3】:

使用C# ValueTupleC# 8.0 Ranges and indices 以及新的switch expressions,可以通过创建方便的extension method 以一种巧妙的方式完成解决方案:

/// <summary>
/// Integer List array extensions.
/// </summary>
public static class RangesExtension
{
    /// <summary>
    /// Creates list of ranges from single dimension ranges list.
    /// </summary>
    /// <param name="ranges">Single dimension ranges list.</param>
    /// <param name="from">Range first item.</param>
    /// <param name="to">Range last item.</param>
    /// <returns>List of ranges.</returns>
    public static List<(int, int)> ToListOfRanges(
        this List<int> ranges,
        int from,
        int to)
    {
        var list = ranges.ToArray();

        return list.OrderBy(item => item)
                   .Select((item, index) => GetNext(ranges, from, index))
                   .Append((list[^1], to))
                   .ToList();
    }

    /// <summary>
    /// Returns next couple of ranges from initial array.
    /// </summary>
    /// <param name="ranges">Single dimension ranges list.</param>
    /// <param name="from">Range first item.</param>
    /// <param name="index">Range current item index.</param>
    /// <returns>Value Tuple of ranges.</returns>
    private static (int, int) GetNext(
        List<int> ranges,
        int from,
        int index)
    {
        return index switch
        {
            0 => (from, ranges[index] - 1),
            _ => (ranges[index - 1], ranges[index] - 1),
        };
    }
}

【讨论】:

    【解决方案4】:

    尝试使用 Linq;假设范围的类型为Tuple&lt;int, int&gt;

    List<int> list = new List<int>() { 8, 22, 41};
    
    int from = 1;
    int upTo = 47;
    
    var result = list
      .OrderBy(item => item)          // to be on the safe side in case of {22, 8, 41}  
      .Concat(new int[] { upTo + 1 }) // add upTo breaking point
      .Select(item => new Tuple<int, int>(from, (from = item) - 1));
    //  .ToArray(); // in case you want to get materialized result (array)
    
    Console.Write(String.Join(Environment.NewLine, result));
    

    结果:

    (1, 7)
    (8, 21)
    (22, 40)
    (41, 47)
    

    【讨论】:

    • 棘手的部分是(from = item) - 1。你必须知道,C# 不仅有赋值语句,还有赋值表达式,它产生赋值作为结果,并将赋值作为副作用。
    • 是的,这令人困惑。
    • 永远不要这样做。永远不要编写一个在运行时改变本地的查询!这是非常危险的。尽管在这种特殊情况下它有效,但即使是对此类查询的微小更改也会开始产生疯狂的结果。
    • @johnny5:是的,正是因为它是一个闭包,我们才有了问题!让我们看一些例子。假设我们有一个字符串序列,我们希望有一个整数序列,我们用零替换非整数。所以我们需要一个bool 来说明一个字符串是否是一个int,如果它是一个值,嘿,TryParse 这样做,但它要求我们改变一个本地。所以我们愚蠢地写var seq = new[]{ "1", "blah", "3" }; int tmp = 0; var nums = from item in seq let success = int.TryParse(item, out tmp) select success ? tmp : 0; ...并且它有效。
    • @johnny5:现在我们应该如何正确解决这个问题?我们通过在另一种方法中完全改变本地来避免改变它: static int? MyTryParse(this string item) { int tmp; bool success = int.TryParse(item, out tmp); return success ? (int?)tmp : (int?)null; } 现在我们的查询是var nums = from item in seq select item.MyTryParse() ?? 0; 并且现在可以根据我们的心脏内容编辑查询,而不必担心我们会介绍由于闭包语义导致的错误,因为没有闭包语义。
    【解决方案5】:

    您没有指定目标语言。因此,在 Scala 中,这样做的一种方法是:

    val breaks = List(8, 22, 41)
    val range = (1, 47)
    
    val ranges = (breaks :+ (range._2 + 1)).foldLeft((range._1, List.empty[(Int, Int)])){
      case ((start, rangesAcc), break) => (break, rangesAcc :+ (start, break - 1))
    }
    
    println(ranges._2)
    

    哪个打印:List((1,7), (8,21), (22,40), (41,47))

    或者,我们可以使用递归:

    def ranges(rangeStart: Int, rangeEnd: Int, breaks: List[Int]) = {
      @tailrec
      def ranges(start: Int, breaks: List[Int], rangesAcc: List[(Int, Int)]): List[(Int, Int)] = breaks match {
        case break :: moreBreaks => ranges(break, moreBreaks, rangesAcc :+ (start, break - 1))
        case nil => rangesAcc :+ (start, rangeEnd)
      }
      ranges(rangeStart, breaks, List.empty[(Int, Int)])
    }
    

    【讨论】:

    • 您没有指定目标语言。
    猜你喜欢
    • 1970-01-01
    • 2018-04-28
    • 1970-01-01
    • 2013-01-01
    • 1970-01-01
    • 2016-12-26
    • 1970-01-01
    • 2015-09-24
    • 1970-01-01
    相关资源
    最近更新 更多