C#算法重构:将数组分成三个部分?

5
我有一个IEnumerable,希望用以下业务逻辑将数据分成3列。如果有3个或更少的项,则每列1个项目;否则,我希望将总项目数除以3,将剩余项(1或2项)分配给前两列。现在这很难看,但它确实完成了工作。我正在寻求有关如何更好地利用linq或可能消除switch语句的技巧。欢迎提供任何改进代码的建议或技巧。
var numItems = items.Count;

            IEnumerable<JToken> col1Items,
                                col2Items, 
                                col3Items;


            if(numItems <=3)
            {
                col1Items = items.Take(1);
                col2Items = items.Skip(1).Take(1);
                col3Items = items.Skip(2).Take(1);

            } else {

                int remainder = numItems % 3,
                    take = numItems / 3,
                    col1Take, 
                    col2Take, 
                    col3Take;

                switch(remainder)
                {
                    case 1:
                        col1Take = take + 1;
                        col2Take = take;
                        col3Take = take;
                        break;
                    case 2:
                        col1Take = take + 1;
                        col2Take = take + 1;
                        col3Take = take;
                        break;
                    default:
                        col1Take = take;
                        col2Take = take;
                        col3Take = take;
                        break;

                }

                col1Items = items.Take(col1Take);
                col2Items = items.Skip(col1Take).Take(col2Take);
                col3Items = items.Skip(col1Take + col2Take).Take(col3Take);

最终我会在MVC Razor视图中使用它们。
<div class="widgetColumn">
                @Html.DisplayFor(m => col1Items, "MenuColumn")                       
            </div> 

            <div class="widgetColumn">
                @Html.DisplayFor(m => col2Items, "MenuColumn")                       
            </div> 

            <div class="widgetColumn">
                @Html.DisplayFor(m => col3Items, "MenuColumn")                       
            </div>  

在我的第一次尝试中,我想摆脱colNItems和colNTake变量,但我无法找到正确的算法使其具有相同的功能。

for (int i = 1; i <= 3; i++ )
            {
                IEnumerable<JToken> widgets = new List<JToken>();
                var col = i;
                switch(col)
                {
                    case 1:
                       break;
                    case 2:
                        break;
                    case 3:
                        break;
                }
            }

5
这个问题可能更适合发布在http://codereview.stackexchange.com/上进行代码审查。 - lc.
思考递归!剩余规则原则上与“小于3”的规则相同,而后者又类似于除法规则。 - Polity
9个回答

6

这些列是固定宽度的吗?如果是,那么您无需对集合做任何特殊处理。只需依靠浏览器为您完成即可。有一个外部容器,其总宽度为3列,然后只需用每个项目的div填充它(并向左浮动)。将内部容器设置为外部容器的1/3宽度。

这里是一个快速fiddle

这是样式的一个快速提示。

div#outer{
    width:300px;    
}

div#outer > div{
    width:100px;
    float:left;    
}

关于这个解决方案,他们希望将元素“垂直”切片,如果可以的话,比如说1-任何物品都会显示在同一列中,而不是像小提琴中那样水平。 解决方案非常好! - Hcabnettek

1
“你不能只是做类似这样的事情吗?”
int len = numItems / 3;
int rem = numItems % 3;

int col1Take = len + (rem > 0 ? 1 : 0);
int col2Take = len + (rem > 1 ? 1 : 0);
int col3Take = len;

编辑:

一种适用于任意列数(COLUMNS)的更通用解决方案是:

int len = numItems / COLUMNS;
int rem = numItems % COLUMNS;

foreach (var i in Enumerable.Range(0, COLUMNS)) {
  colTake[i] = len + (rem > i ? 1 : 0);
}

这没有意义,你只是在计算项目吗?难道你不想把结果放在一个列表中吗? - Hogan
如果您仔细阅读原始帖子的全部内容,您会发现问题是“是否有方法可以摆脱switch语句?”我已经使用这段代码片段完成了它。 - Andrew Coonce
我明白你的意思,不过我认为他需要一个能让列表更快的解决方案。 - Hogan
我假设性能不是问题(因为它很少真正是)。相反,我专注于可维护性、可重用性和糟糕的代码味道。我发布了第一个解决方案,因为它确实易于维护并解决了最严重的代码味道问题,但第二个解决方案在可重用性方面进行了一些可读性的牺牲,所以我也包括了它。各有所好。 - Andrew Coonce
在这种情况下,Hogan的性能并不是问题。外部集合永远不会超过20-30个项目。谢谢大家! - Hcabnettek

1

你可以泛化:

int cols = 3;
IEnumerable<JToken> colItems[3]; // you can make this dynamic of course

int rem = numItems % cols;
int len = numItems / cols;

for (int col=0; col<cols; col++){
    int colTake = len;
    if (col < rem) colTake++;
    colItems[col] = items.Skip(col*len).Take(colTake);
}

尚未测试,但这应该适用于任意数量的列。

另外,每当您需要变量col1、col2、col3时,请考虑col[0]、col[1]、col[2]。


这不会创建3个列表吗? - Hogan
代码现在创建了列表。我认为这符合原帖作者的想法,即将连续的元素放在同一列中。 - rslite
我最终采用了这种方法。看起来足够简洁明了。不过大家提供的所有解决方案都很好。我也非常喜欢 Brian Ball 的纯 CSS 方法。谢谢大家! - Hcabnettek

1

所以您想要第一列中的前n/3个项目,第二列中的下一个n/3个项目,依此类推。

var concreteList = items.ToList();
var count = concreteList.Count;
var take1 = count/3 + (count % 3 > 0 ? 1 : 0);
var take2 = count/3 + (count % 3 > 1 ? 1 : 0);

var col1 = concreteList.Take(take1);
var col2 = concreteList.Skip(take1).Take(take2);
var col3 = concreteList.Skip(take1 + take2);

我制作了一个具体的列表,以避免多次迭代Enumerable。例如,如果你有:

items = File.ReadLines("foo.txt");

那么您将无法多次迭代它。

0

如果您想要循环填充列,可以使用以下方法:

int numColumns = 3;

var result = Enumerable.Range(1,numColumns).Select(c =>
      items.Where((x,ix) => ix % numColumns == c-1).ToArray()
   );

可爱,但你迭代列表三次而不是一次。 - Hogan

0

这可能会有所帮助

        IEnumerable<object> items = new Object[]{ "1", "2", "3", "4", "5", "6", "7","8", "9", "10", "11", "12","13", "14" };

        IEnumerable<object> col1Items = new List<object>(),
                            col2Items = new List<object>(), 
                            col3Items = new List<object>();

        Object[] list = new Object[]{col1Items, col2Items, col3Items};
        int limit = items.Count()/3;
        int len = items.Count();
        int col;            

        for (int i = 0; i < items.Count(); i++ )
        {                
            if (len == 3) col = i;
            else col = i / limit;

            if (col >= 3) col = i%limit ;

            ((IList<object>)(list[col])).Add( items.ElementAt(i));

        }

0
如果你想先横向再纵向,可以看下面的答案。如果你想先纵向再横向,可以使用以下代码(将其与下面的测试一起使用以查看其工作效果,而不是使用var result行):
var curCol = 0;
var iPer = items.Count() / 3;
var iLeft = items.Count() % 3;
var result = items.Aggregate(
               // object that will hold items
               new {  
                      cols = new List<ItemElement>[3] { new List<ItemElement>(), 
                                                        new List<ItemElement>(), 
                                                        new List<ItemElement>(), },
                          },
               (o, n) => {
                 o.cols[curCol].Add(n);

                 if (o.cols[curCol].Count() > iPer + (iLeft > (curCol+1) ? 1:0))
                   curCol++;

                 return new {
                   cols = o.cols
                };
             });

你可以使用聚合函数来实现。代码如下:

void Main()
{
  List<ItemElement> items = new List<ItemElement>() { 
           new ItemElement() { aField = 1 },
           new ItemElement() { aField = 2 },
           new ItemElement() { aField = 3 },
           new ItemElement() { aField = 4 },
           new ItemElement() { aField = 5 },
           new ItemElement() { aField = 6 },
           new ItemElement() { aField = 7 },
           new ItemElement() { aField = 8 },
           new ItemElement() { aField = 9 }
  };

  var result = 
    items.Aggregate(
      // object that will hold items
      new {  
        cols = new List<ItemElement>[3] { new List<ItemElement>(), 
                                          new List<ItemElement>(), 
                                          new List<ItemElement>(), },
        next = 0 },
     // aggregate
     (o, n) => {
       o.cols[o.next].Add(n);

       return new {
         cols = o.cols,
         next = (o.next + 1) % 3
       };
    });
  result.Dump();
}

public class ItemElement 
{
   public int aField { get; set; }
}

你最终得到一个对象,其中包含3个列表的数组(每列一个列表)。

这个例子可以在linqPad中直接运行。我推荐使用linqPad进行这种POC测试。(linqPad.com)


0
LinqLib(nuget:LinqExtLibrary)有一个重载的ToArray()可以实现此功能:
using System.Collections.Generic;
using System.Linq;
using LinqLib.Array;

...

    public void TakeEm(IEnumerable<int> data)
    {
        var dataAry = data as int[] ?? data.ToArray();
        var rows = (dataAry.Length/3) + 1;
        //var columns = Enumerable.Empty<int>().ToArray(3, rows);
        // vvv These two lines are the ones that re-arrange your array
        var columns = dataAry.ToArray(3, rows);
        var menus = columns.Slice();
    }

0

这不是很快,但它能解决问题:

var col1Items = items.Select((obj, index) => new { Value = obj, Index = index })
    .Where(o => o.Index % 3 == 0).Select(o => o.Value);
var col2Items = items.Select((obj, index) => new { Value = obj, Index = index })
    .Where(o => o.Index % 3 == 1).Select(o => o.Value);
var col3Items = items.Select((obj, index) => new { Value = obj, Index = index })
    .Where(o => o.Index % 3 == 2).Select(o => o.Value);

它使用包括索引参数的Select版本。您可以使用GroupBy来加快速度,但代价是需要写入一些额外的代码。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接