繁体   English   中英

如何使用 Ramda JS 在排序列表中获取每组前 n 个元素

[英]How do I get the top n elements per group within a sorted list with Ramda JS

我对 RamdaJS 和函数式编程还很陌生,所以我希望了解是否有更有效的方法来实现以下目标。

假设我有一个足球运动员名单。 列表中的每个足球运动员都有其姓名、球队和月薪的属性。

const footballers = [
  {
    name: "Mesut Ozil",
    team: "Arsenal",
    pay: 1400000
  },
  {
    name: "Lionel Messi",
    team: "Barcelona",
    pay: 7300000
  },
  {
    name: "Kylian Mbappe",
    team: "PSG",
    pay: 1500000
  },
  {
    name: "Alexis Sanchez",
    team: "Manchester United",
    pay: 1990000
  },
  {
    name: "Philippe Coutinho",
    team: "Barcelona",
    pay: 2000000
  },
  {
    name: "Neymar",
    team: "PSG",
    pay: 2700000
  },
  {
    name: "Luis Suarez",
    team: "Barcelona",
    pay: 2500000
  },
  {
    name: "Antoine Griezmann",
    team: "Atletico Madrid",
    pay: 2900000
  },
  {
    name: "Gareth Bale",
    team: "Real Madrid",
    pay: 2200000
  },
  {
    name: "Cristiano Ronaldo",
    team: "Juventus",
    pay: 4100000
  }
]

我希望按薪水排序这份名单,但限制每支球队在最终名单中最多只能有两名球员。 目前我的解决方案必须对列表进行两次排序。 目前是先排序,后排序,也可以分组排序。 我相信有一个更聪明(但仍然可读)的解决方案,它只需要一种,但我不确定它是什么。

const byPayAsc = ascend(prop('pay')) // spare, for checking
const byPayDes = descend(prop('pay'))
const sortByPay = sort(byPayDes)

const groupedByTeam = compose(values, groupBy(prop('team')))
const topTwoPlayers = map(take(2))

const topTwoHighestPaidPlayersPerTeam = pipe( 
  sortByPay,
  groupedByTeam, 
  topTwoPlayers, 
  flatten, 
  sortByPay
)

topTwoHighestPaidPlayersPerTeam(footballers)

我目前的调查发现了几个选项:

  • 我听说过换能器,我认为它可能提供只循环一次列表的好处。
  • 我还想知道是否可以使用我的排序功能来合并分组列表?
  • 或者我可以编写一个 reducer 来循环遍历列表列表并获取每个团队的前 2 个。

使用 Ramda JS 执行此操作的惯用方法是什么。

单一排序解决方案

实际上确实避免了第二种方法的方法当然是可能的。 这是一个这样做的版本:

 const topTwoHighestPaidPlayersPerTeam = pipe ( sort (descend (prop ('pay'))), reduce ( ({result, counts}, player, {team} = player) => ({ result: counts [team] >= 2 ? result : [...result, player], counts: {...counts, [team]: (counts[team] || 0) + 1} }), {result: [], counts: {}} ), prop ('result') ) const footballers = [{"name":"Mesut Ozil","team":"Arsenal","pay":1400000},{"name":"Lionel Messi","team":"Barcelona","pay":7300000},{"name":"Kylian Mbappe","team":"PSG","pay":1500000},{"name":"Alexis Sanchez","team":"Manchester United","pay":1990000},{"name":"Philippe Coutinho","team":"Barcelona","pay":2000000},{"name":"Neymar","team":"PSG","pay":2700000},{"name":"Luis Suarez","team":"Barcelona","pay":2500000},{"name":"Antoine Griezmann","team":"Atletico Madrid","pay":2900000},{"name":"Gareth Bale","team":"Real Madrid","pay":2200000},{"name":"Cristiano Ronaldo","team":"Juventus","pay":4100000}] const result = topTwoHighestPaidPlayersPerTeam(footballers) console .log (result)
 <script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script> <script> const {pipe, sort, descend, prop, reduce} = R </script>

我们从按薪酬的标准排序开始,然后遍历它们,跟踪每个团队的计数以及结果,每当计数小于我们的最大值 2 时添加团队。我们通过累积一个对象来跟踪这一点结果和计数。 最后,我们只提取results属性。 虽然这可以用filter而不是reduce来完成,但传递给filter的函数需要保持counts状态,这似乎不适合filter

稍微明显的版本

      result: counts [team] < 2 ? [...result, player] : result, 

将不起作用,因为最初团队计数未定义,并且undefined < 2产生false 如果看起来更清楚,我们可以选择这个版本:

      result: (counts [team] || 0) < 2 ? [...result, player] : result, 

为什么我们可能不应该使用这个解决方案

此代码确实避免了第二次排序。 但是这样做的代价是可读性,并且可能对于合理大小的集合,在实际性能方面。

二分法还是O (n log (n)) 做两次事情不会改变总体绩效指标。 所以这个版本并不比二分类快。 但是代码的可读性远不及 Scott Christopher 或 Ori Drori 的代码。 除非我们已经测量并可以指出特定的瓶颈,否则出于性能原因增加代码的复杂性似乎完全是浪费。

所以我会推荐 Ori Drori 的解决方案。 Scott Christopher 也有一个有趣的方法。

但是这种技术对于在折叠值列表时维护一些额外的状态仍然很有用。

您再次排序的想法是有效的,因为按团队对玩家进行分组会改变初始排序。

如果你想跳过第二次排序,你需要保留每个项目的原始索引,然后按索引排序。 所以我不确定这是否值得。

然而,为了检查这个想法,这个片段在排序之后,但在玩家的团队分组之前将数组项转换为 [index, player] 对。 当您使用 R.values 和 R.chain(使用 R.take)将组展平回数组时,您可以使用 R.fromPairs 将这些对转换回对象。 由于 ES6 对整数对象键的遍历是升序的,因此恢复了原始顺序,现在您再次通过调用 R.values 来获取数组。

 const { pipe, sort, descend, prop, toPairs, groupBy, path, values, chain, take, fromPairs } = R const topTwoHighestPaidPlayersPerTeam = pipe( sort(descend(prop('pay'))), // sort descending by pay toPairs, // convert to pairs if [index, player object] groupBy(path([1, 'team'])), // group by the team values, // get an array of an arrays chain(take(2)), // flatten and take the 1st two items of each group fromPairs, // convert to an object of objects with original index as key values // convert to an array in the correct order ) const footballers = [{"name":"Mesut Ozil","team":"Arsenal","pay":1400000},{"name":"Lionel Messi","team":"Barcelona","pay":7300000},{"name":"Kylian Mbappe","team":"PSG","pay":1500000},{"name":"Alexis Sanchez","team":"Manchester United","pay":1990000},{"name":"Philippe Coutinho","team":"Barcelona","pay":2000000},{"name":"Neymar","team":"PSG","pay":2700000},{"name":"Luis Suarez","team":"Barcelona","pay":2500000},{"name":"Antoine Griezmann","team":"Atletico Madrid","pay":2900000},{"name":"Gareth Bale","team":"Real Madrid","pay":2200000},{"name":"Cristiano Ronaldo","team":"Juventus","pay":4100000}] const result = topTwoHighestPaidPlayersPerTeam(footballers) console.log(result)
 <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>

您在这里拥有的内容接近于合并排序,每个团队执行初始排序,然后合并排序后的球员列表。

 const { descend, prop, take, sort, reduceBy, pipe, values, reduce } = R const sorter = descend(prop('pay')) // this is used by `R.reduceBy` below to build up the sorted list with a max size const addToGroup = (size, compare) => (group, a) => take(size, sort(sorter, [a, ...group])) const sortByTeam = reduceBy(addToGroup(2, sorter), [], prop('team')) // recursively merges two sorted(!) lists by the given comparator const mergeListsBy = compare => { const go = (xs, ys) => xs.length == 0 ? ys : ys.length == 0 ? xs : compare(xs[0], ys[0]) < 0 ? [xs[0], ...go(xs.slice(1), ys)] : [ys[0], ...go(xs, ys.slice(1))] return go } const run = pipe(sortByTeam, values, reduce(mergeListsBy(sorter), [])) //// const footballers = [{"name": "Mesut Ozil", "pay": 1400000, "team": "Arsenal"}, {"name": "Lionel Messi", "pay": 7300000, "team": "Barcelona"}, {"name": "Kylian Mbappe", "pay": 1500000, "team": "PSG"}, {"name": "Alexis Sanchez", "pay": 1990000, "team": "Manchester United"}, {"name": "Philippe Coutinho", "pay": 2000000, "team": "Barcelona"}, {"name": "Neymar", "pay": 2700000, "team": "PSG"}, {"name": "Luis Suarez", "pay": 2500000, "team": "Barcelona"}, {"name": "Antoine Griezmann", "pay": 2900000, "team": "Atletico Madrid"}, {"name": "Gareth Bale", "pay": 2200000, "team": "Real Madrid"}, {"name": "Cristiano Ronaldo", "pay": 4100000, "team": "Juventus"}] console.log(run(footballers))
 <script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>

为简单起见,我只编写了一个自定义filter函数...

 const playersPerTeam = (n) => { const teams = {}; const predicate = (({ team }) => { teams[team] = (teams[team] || 0) + 1; return teams[team] <= n; }); return R.filter(predicate); }; const fn = R.pipe( R.sort(R.descend(R.prop('pay'))), playersPerTeam(2), ); // ------ const data = [ { name: "Mesut Ozil", team: "Arsenal", pay: 1400000 }, { name: "Lionel Messi", team: "Barcelona", pay: 7300000 }, { name: "Kylian Mbappe", team: "PSG", pay: 1500000 }, { name: "Alexis Sanchez", team: "Manchester United", pay: 1990000 }, { name: "Philippe Coutinho", team: "Barcelona", pay: 2000000 }, { name: "Neymar", team: "PSG", pay: 2700000 }, { name: "Luis Suarez", team: "Barcelona", pay: 2500000 }, { name: "Antoine Griezmann", team: "Atletico Madrid", pay: 2900000 }, { name: "Gareth Bale", team: "Real Madrid", pay: 2200000 }, { name: "Cristiano Ronaldo", team: "Juventus", pay: 4100000 }, ]; console.log( fn(data), );
 <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js" integrity="sha256-xB25ljGZ7K2VXnq087unEnoVhvTosWWtqXB4tAtZmHU=" crossorigin="anonymous"></script>

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM