隐藏列表<int>到范围列表

时间:2018-08-29 18:37:22

标签: c# list range

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

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

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

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

我试图从此question 适应解决方案,但无法使其正常工作。

似乎应该很简单,但也许不是。

5 个答案:

答案 0 :(得分:5)

即使在程序运行时,指出在查询期间应更改本地变量的答案也是危险的;不要养成这个坏习惯。

解决问题的最简单方法是编写迭代器块。假设您有明显的Pair<T>类型;在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),,依此类推。

答案 1 :(得分:1)

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

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 }); 

如果您想要替代的pure-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();
}

答案 2 :(得分:1)

使用C# ValueTupleC# 8.0 Ranges and indices以及新的switch expressions,可以通过创建方便的{{3 }}:

/// <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),
        };
    }
}

答案 3 :(得分:0)

尝试使用 Linq ;假设范围为Tuple<int, int>类型:

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)

答案 4 :(得分:-1)

您没有指定目标语言。因此,在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)])
}