【问题标题】:reduce array of arrays into on array in collated order按整理顺序将数组数组减少到数组中
【发布时间】:2019-07-14 17:58:24
【问题描述】:

我正在尝试使用reduce() 以“整理”顺序组合一组数组,以便具有相似索引的项目放在一起。例如:

input = [["one","two","three"],["uno","dos"],["1","2","3","4"],["first","second","third"]]

output = [ 'first','1','uno','one','second','2','dos','two','third','3','three','4' ]

索引相似的项目的顺序并不重要,只要它们在一起,所以'one','uno','1'...的结果和上面的一样好。如果可能的话,我想只使用不可变变量。

我有一个可行的方法:

    const output = input.reduce((accumulator, currentArray, arrayIndex)=>{
        currentArray.forEach((item,itemIndex)=>{
            const newIndex = itemIndex*(arrayIndex+1);
            accumulator.splice(newIndex<accumulator.length?newIndex:accumulator.length,0,item);
        })
        return accumulator;
    })

但它不是很漂亮,我也不喜欢它,特别是因为它改变了 forEach 方法中的累加器的方式。我觉得一定有更优雅的方法。

我不敢相信以前没有人问过这个问题,但我尝试了很多不同的查询但找不到它,所以请告诉我它是否存在并且我错过了它。有没有更好的办法?

为了澄清 cmets 中的每个问题,我希望能够在不改变任何变量或数组的情况下做到这一点,就像我对 accumulator.splice 所做的那样,并且只使用函数方法,例如 .map 或 @987654327 @ 不是像 .forEach 这样的变异循环。

【问题讨论】:

  • “有更好的方法吗?”“更好”是什么意思? “我不喜欢” 不是客观的编码问题。有什么要求?
  • 好吧,我想做的一件事是不改变任何变量,或者引入一个没有真正功能的.forEach 循环。而且我认为它也可以做得更简洁。如果我可以像在 Python 中一样将一组数组压缩在一起,那么这样减少(连接)它们也可以。
  • 我想我很清楚地说我希望结果不会改变任何数组或变量,就像我对accumulator.splice 所做的那样。这是客观要求。
  • 该要求是否包括限制使用JSON.parse(JSON.stringify(input)) 以避免改变原始数组?
  • 我从所有这些答案中学到了很多。我很难选择一个作为“解决方案”,因为有很多有趣的方法可以解决这个问题,所以我觉得只选择一个很糟糕。我选择的那个最接近我的想法,虽然不一定是最好的。谢谢大家。

标签: javascript arrays functional-programming


【解决方案1】:
Array.prototype.coallate = function (size) {
  return [...Array(Math.ceil(this.length / size)).keys()].map(i => this.slice(i * size, size * (i + 1)));
};
const result = [0,1,2,3,4,5,6,7,8,9].coallate(3)


console.log(JSON.stringify(result));

结果: [[0,1,2],[3,4,5],[6,7,8],[9]]

【讨论】:

    【解决方案2】:

    我已经看到名为round-robin 的问题,但也许interleave 是一个更好的名称。当然,mapreducefilter 是函数式过程,但并非所有函数式程序都需要依赖它们。当这些是我们知道如何使用的唯一功能时,生成的程序有时会很尴尬,因为通常有更好的适合。

    • map 产生一对一的结果。如果我们有 4 个子数组,我们的结果将有 4 个元素。 interleave 应该产生一个等于组合子数组长度的结果,所以 map 可能只能让我们部分到达那里。需要额外的步骤才能获得最终结果。

    • reduce 一次一个地遍历输入元素以产生最终结果。在第一个 reduce 中,我们将得到第一个子数组,但是在移动到下一个子数组之前没有直接的方法来处理整个子数组。我们可以强制我们的程序使用reduce,但这样做会使我们将整理过程视为一个回溯过程,而不是它实际上是什么。

    现实情况是,您不仅限于使用这些原始的功能程序。您可以以直接编码其意图的方式编写interleave。我认为interleave 有一个漂亮的递归定义。我认为在这里使用深度解构赋值很好,因为函数的签名显示了interleave 期望的数据的形状;数组数组。 Mathematical induction 让我们可以自然地处理程序的分支 -

    const None =
      Symbol ('None')
    
    const interleave =
      ( [ [ v = None, ...vs ] = []  // first subarray
        , ...rest                   // rest of subarrays
        ]
      ) =>
        v === None
          ? rest.length === 0
            ? vs                                   // base: no `v`, no `rest`
            : interleave (rest)                    // inductive: some `rest`
          : [ v, ...interleave ([ ...rest, vs ]) ] // inductive: some `v`, some `rest`
    
    const input =
      [ [ "one", "two", "three" ]
      , [ "uno", "dos" ]
      , [ "1", "2", "3", "4" ]
      , [ "first", "second", "third" ]
      ]
    
    console.log (interleave (input))
    // [ "one", "uno", "1", "first", "two", "dos", "2", "second", "three", "3", "third", "4" ]

    interleave 让我们摆脱了思维狭隘的束缚。我不再需要从笨拙地组合在一起的畸形片段来考虑我的问题——我不是在考虑数组索引、sort、或forEach,或者用push 改变状态,或者使用进行比较&gt;Math.max。我也不必考虑像 array-like 这样不正当的事情——哇,我们真的理所当然地认为我们对 JavaScript 的了解有多少!

    在上面,没有 依赖项应该让人耳目一新。想象一个初学者接近这个程序:他/她只需要学习 1)如何定义一个函数,2)解构语法,3)三元表达式。由无数小依赖拼凑在一起的程序需要学习者熟悉每个程序,然后才能获得对该程序的直觉。

    也就是说,用于解构值的 JavaScript 语法并不是最漂亮的,有时为了方便而进行交易以提高可读性 -

    const interleave = ([ v, ...vs ], acc = []) =>
      v === undefined
        ? acc
    : isEmpty (v)
        ? interleave (vs, acc)
    : interleave
        ( [ ...vs, tail (v) ]
        , [ ...acc, head (v) ]
        )
    

    这里演变的依赖是isEmptytailhead -

    const isEmpty = xs =>
      xs.length === 0
    
    const head = ([ x, ...xs ]) =>
      x
    
    const tail = ([ x, ...xs ]) =>
      xs
    

    功能相同-

    const input =
      [ [ "one", "two", "three" ]
      , [ "uno", "dos" ]
      , [ "1", "2", "3", "4" ]
      , [ "first", "second", "third" ]
      ]
    
    console.log (interleave (input))
    // [ "one", "uno", "1", "first", "two", "dos", "2", "second", "three", "3", "third", "4" ]
    

    在下面您自己的浏览器中验证结果-

    const isEmpty = xs =>
      xs.length === 0
      
    const head = ([ x , ...xs ]) =>
      x
      
    const tail = ([ x , ...xs ]) =>
      xs
      
    const interleave = ([ v, ...vs ], acc = []) =>
      v === undefined
        ? acc
    : isEmpty (v)
        ? interleave (vs, acc)
    : interleave
        ( [ ...vs, tail (v) ]
        , [ ...acc, head (v) ]
        )
    
    const input =
      [ [ "one", "two", "three" ]
      , [ "uno", "dos" ]
      , [ "1", "2", "3", "4" ]
      , [ "first", "second", "third" ]
      ]
    
    console.log (interleave (input))
    // [ "one", "uno", "1", "first", "two", "dos", "2", "second", "three", "3", "third", "4" ]

    如果您开始考虑使用mapfilterreduce 来考虑interleave,那么它们很可能会成为最终解决方案的一部分。如果这是您的方法,那么您应该感到惊讶的是,mapfilterreduce 在此答案的两个程序中无处可见。这里的教训是你成为你所知道的东西的囚徒。您有时需要忘记mapreduce,以便观察其他问题具有独特的性质,因此一种通用方法虽然可能有效,但不一定是最合适的。

    【讨论】:

    • v === undefined 模式是测试数组是否为空的糟糕方法。如果输入是一个稀疏数组,比如[,2,3],怎么办?测试数组是否为空的唯一可靠方法是检查其length 属性。
    • 这取决于您的程序中是否可以使用稀疏数组。我认为稀疏数组很危险,所以我不使用它们。如果我的程序由于undefined 值而具有undefined 行为,我可以接受;这意味着undefined将会发生。
    • 是的。有时最好不要考虑这种罕见的边缘情况,并且能够进行模式匹配可能不值得考虑稀疏数组。如果有一种方法可以在 JavaScript 中进行正确的模式匹配,那就太好了。这是我想出的解决方案:const collate = (vvs, acc = [], [v, ...vs] = vvs) =&gt; vvs.length === 0 ? acc : ....。这是对默认参数的严重滥用,但它可以在仍然使用稀疏数组的同时实现简洁的模式匹配。
    • 顺便说一下,定义函数的另一种方法(使用大步骤而不是小步骤)如下:const collate = (xss, yss = xss.filter(xs =&gt; xs.length &gt; 0)) =&gt; yss.length === 0 ? [] : yss.map(([x, ...xs]) =&gt; x).concat(collate(yss.map(([x, ...xs]) =&gt; xs)));
    • 感谢轮询问题的参考。你是对的,通过专注于现有的方法,如 map 和 reduce,我限制了自己。这是非常优雅的。我将对其进行一些研究,以确保我真正理解您使用自己的 tailrest 方法所做的事情。感谢您抽出宝贵时间提供这个出色的解决方案和解释。
    【解决方案3】:

    使用Array.from() 创建一个新数组,其长度为最长子数组的长度。要获取最长子数组的长度,请使用Array.map() 获取长度数组并取最大值。

    Array.from() 的回调中使用Array.reduceRight()Array.reduce()(取决于您想要的顺序)从每个子数组中收集项目。如果当前索引存在于子数组中,则取该项。使用Array.flat() 展平子数组。

    const input = [["one","two","three"],["uno","dos"],["1","2","3","4"],["first","second","third"]]
    
    const result = Array.from(
        { length: Math.max(...input.map(o => o.length)) },
        (_, i) => input.reduceRight((r, o) =>
          i < o.length ? [...r, o[i]] : r
        , [])
      )
      .flat();
    
    console.log(result);

    【讨论】:

    • 酷!我喜欢!
    • -4 字节 [...Array(Math.max(...input.map(o =&gt; o.length)))].map((_,i)=&gt;{})
    • @guest271314 - 之后您需要填充数组,因为您无法映射稀疏项目。
    • @OriDrori 不确定你的意思。您一定没有尝试过代码。 [...Array(N)]Array.from({length:N}) 具有相同的输出。唯一的区别是[...Array(N)].map((_,i)=&gt;{})Array.from({length:N},(_,i)=&gt;{}) 少4 个字节。
    • 如果我想更改具有相同索引的项目一起出现的顺序,感谢您建议 reduceRight。这是一个非常好的解决方案。
    【解决方案4】:

    这就是我想出的......虽然现在在看到其他答案之后,这个解决方案似乎更笨重......而且它仍然使用 forEach。我很想知道不使用 forEach 的好处。

    var input = [["1a", "2a", "3a"], ["1b"], ["1c", "2c", "3c", "4c", "5c", "6c", "7c"],["one","two","three","four","five","six","seven"],["uno","dos","tres"],["1","2","3","4","5","6","7","8","9","10","11"],["first","second","third","fourth"]];
    
    // sort input array by length
    input = input.sort((a, b) => {
      return b.length - a.length;
    });
    
    let output = input[0];
    let multiplier = 2;
    
    document.writeln(output + "<br>");
    
    input.forEach((arr, i1) => {
      if (i1 > 0) {
        let index = -1;
        arr.forEach((item) => {  
          index = index + multiplier;
          output.splice(index, 0, item);        
        });
        
        document.writeln(output + "<br>");
        multiplier++;
      }
    });

    【讨论】:

    • 老实说,我猜 forEach 循环和变量突变并没有真正伤害任何东西,因为我们只是在循环内改变了一次性累加器变量。我想我对使用纯函数式方法很感兴趣,因为如果您可以使用简单的循环,那么有一个非常简单的解决方案,其中涉及到 while 循环中的 for 循环,那么为什么要使用 reduce 呢?
    【解决方案5】:

    这个怎么样?

    • 对数组进行排序,将最长的放在第一位。
    • flatMap 它们,因此最长数组中每个项目的索引获取xs 中任何其他数组的每个索引。
    • 过滤掉平面数组中的undefined 项(通过使索引超出每个可能数组的范围而产生的项)

    const input = [
      ["one", "two", "three"],
      ["uno", "dos"],
      ["1", "2", "3", "4"],
      ["first", "second", "third"]
    ]
    
    const arrayLengthComparer = (a, b) => b.length - a.length
    
    const collate = xs => {
      const [xs_, ...ys] = xs.sort (arrayLengthComparer)
      
      return xs_.flatMap ((x, i) => [x, ...ys.map (y => y[i])])
                .filter (x => x)
    }
    
    const output = collate (input)
    
    console.log (output)
       

    【讨论】:

    • 您可以将比较器功能简化如下:const arrayLengthComparer = (a, b) =&gt; b.length - a.length;.
    • 哦,很好,之前有人建议了一个 flatMap 解决方案,并在我阅读之前将其删除,是你吗?这是一个很好的解决方案。首先将它们分类为头部和尾部真的很聪明。
    • @jimboweb 嘿,谢谢。不,这不是我的答案。真的,我不知道已经有使用flatMap 的答案
    【解决方案6】:

    这是一个符合您指定的优雅标准的递归解决方案:

    const head = xs => xs[0];
    
    const tail = xs => xs.slice(1);
    
    const notNull = xs => xs.length > 0;
    
    console.log(collate([ ["one", "two", "three"]
                        , ["uno", "dos"]
                        , ["1", "2", "3", "4"]
                        , ["first", "second", "third"]
                        ]));
    
    function collate(xss) {
        if (xss.length === 0) return [];
    
        const yss = xss.filter(notNull);
    
        return yss.map(head).concat(collate(yss.map(tail)));
    }

    可以直接翻译成Haskell代码:

    collate :: [[a]] -> [a]
    collate []  = []
    collate xss = let yss = filter (not . null) xss
                  in map head yss ++ collate (map tail yss)
    

    之前的解决方案采用big steps 来计算答案。这是一个递归解决方案,它需要small steps 来计算答案:

    console.log(collate([ ["one", "two", "three"]
                        , ["uno", "dos"]
                        , ["1", "2", "3", "4"]
                        , ["first", "second", "third"]
                        ]));
    
    function collate(xss_) {
        if (xss_.length === 0) return [];
    
        const [xs_, ...xss] = xss_;
    
        if (xs_.length === 0) return collate(xss);
    
        const [x, ...xs] = xs_;
    
        return [x, ...collate(xss.concat([xs]))];
    }

    这是等效的 Haskell 代码:

    collate :: [[a]] -> [a]
    collate []           = []
    collate ([]:xss)     = collate xss
    collate ((x:xs):xss) = x : collate (xss ++ [xs])
    

    希望对您有所帮助。

    【讨论】:

    • let yss = filter (not . null) xss in map head yss ++ collate (map tail yss) === concatMap (take 1) xss ++ collate [t | (_:t) &lt;- xss]。更短。 :)
    • 另外,出于好奇,collate = concat . takeWhile (not . null) . unfoldr (Just . traverse (splitAt 1))
    • 是的,这绝对符合我所希望的优雅标准。我希望我可以选择不止一种解决方案!感谢您将其放入 Haskell 代码中。
    • (还有,concat . transpose,但是(重新)发明比学习更有趣......)
    【解决方案7】:

    我使用递归方法来避免突变。

    let input = [["one","two","three"],["uno","dos"],["1","2","3","4"],["first","second","third"]]
    
    function recursion(input, idx = 0) {
      let tmp = input.map(elm => elm[idx])
                     .filter(e => e !== undefined)
      return tmp[0] ? tmp.concat(recursion(input, idx + 1)) : []
    }
    
    console.log(recursion(input))

    【讨论】:

    • 哦,我喜欢!我什至没有想过使用递归。非常优雅。
    【解决方案8】:

    也许只是一个简单的for... i 循环,它检查每个数组中的位置i 中的项目

    var input = [["one","two","three"],["uno","dos"],["1","2","3","4"],["1st","2nd","3rd"]]
    
    var output = []
    var maxLen = Math.max(...input.map(arr => arr.length));
    
    for (i=0; i < maxLen; i++) {
      input.forEach(arr => { if (arr[i] !== undefined) output.push(arr[i]) })
    }
    
    console.log(output)

    简单,但可预测且可读


    避免每个循环

    如果您需要避免forEach,可以使用类似的方法:获取将由 for 循环 ([1,2,3,4]) 创建的 max child array lengthbuild a range of integers,将每个值映射到枢轴数组,flatten the multi-dimensional array,然后filter 取出空单元格。

    首先是离散步骤,然后是一个单行:

    var input = [["one","two","three"],["uno","dos"],["1","2","3","4"],["1st","2nd","3rd"]];
    

    多个步骤:

    var maxLen = Math.max(...input.map(arr => arr.length));
    var indexes = Array(maxLen).fill().map((_,i) => i);
    var pivoted = indexes.map(i => input.map(arr => arr[i] ));
    var flattened = pivoted.flat().filter(el => el !== undefined);
    

    一个班轮:

    var output = Array(Math.max(...input.map(arr => arr.length))).fill().map((_,i) => i)
                   .map(i => input.map(arr => arr[i] ))
                   .flat().filter(el => el !== undefined)
    

    【讨论】:

    • 谢谢你,这是一个很棒而详细的答案,我学到了很多东西。你是对的,for 循环是最可预测的结果,我想没有理由不这样做。您的功能解决方案很棒。没想到用 Math.max 从最大的数组开始,然后填写索引。
    【解决方案9】:

    在这里,我提供了一个生成器函数,它将按所需顺序生成值。如果将 yield 替换为 push 到要返回的结果数组,则可以轻松地将其转换为返回数组的常规函数​​。

    该算法将所有数组作为参数,然后获取每个数组的迭代器。然后它进入主循环,将iters 数组视为队列,将迭代器放在前面,产生下一个生成值,然后将其放回队列的末尾,除非它为空。如果您将数组转换为链表,其中添加前后需要恒定时间,效率会提高,而数组上的 shift 是将所有内容向下移动一个位置的线性时间。

    function* collate(...arrays) {
      const iters = arrays.map(a => a.values());
      while(iters.length > 0) {
        const iter = iters.shift();
        const {done, value} = iter.next();
        if(done) continue;
        yield value;
        iters.push(iter);
      }
    }
    

    【讨论】:

    • 这是一个很好的答案,谢谢。但是 while 循环有点像我试图绕过的 .forEach 。但这仍然是一个很好的答案,所以我投了赞成票。
    【解决方案10】:

    有趣的解决方案

    1. 在内部数组中添加索引作为前缀
    2. 扁平化数组
    3. 对数组进行排序
    4. 去掉前缀

    let input = [["one","two","three"],["uno","dos"],["1","2","3","4"],["first","second","third"]]
    let ranked=input.map(i=>i.map((j,k)=>k+'---'+j)).slice()
    console.log(ranked.flat().sort().map(i=>i.split('---')[1]));

    【讨论】:

    • 聪明的答案。我确实考虑过做这样的事情。我在想一些更像是让内部地图返回 {ind:k,val:j} 后跟 .sort((a,b)=&gt;return a.ind-b.ind) 的东西,但这是相同的想法。我决定反对它,因为创建不必要的对象(或字符串)感觉就像做不必要的工作。但仍然是一个很好的答案,谢谢。
    猜你喜欢
    • 1970-01-01
    • 2019-09-21
    • 1970-01-01
    • 2023-03-08
    • 2023-02-24
    • 1970-01-01
    • 1970-01-01
    • 2021-04-05
    • 2021-08-07
    相关资源
    最近更新 更多