PAYATHON 2026

允许同一元素进入多个分组的 multi-group-by 操作有通用名称吗?

数据小陈

可以将这个工具命名为 multiGroupBy。文档中应说明,回调返回一组分组键,每个元素会进入这些键对应的所有分组。这个名字能表达需求,但不能据此认定它是跨语言统一的标准 API 名称。

标准库可以通过已有操作的组合实现这种行为。例如,JavaScript 可以使用题目中的 flatMap 加 Object.groupBy,.NET LINQ 可以使用 SelectMany 加 ToLookup。这两种都是组合方案,其中任何一个函数都不能单独等同于所需的 multiGroupBy。

这种分组的语义是什么?

示例中的 x 有两个归属键 a 和 b,因此同一个元素会同时出现在两个结果数组中。接口说明可以将这种行为描述为“允许重叠的分组”或“按多个归属键分组”。这些描述用来说明行为,并非某个标准库规定的术语。

这里需要区分两种需求:

  • 按复合键分组:把多个字段组成一个键,每个元素仍然只进入一个分组。
  • 按多个归属键分组:每个元素产生多个键,并分别进入对应分组。

题目需要的是第二种。仅写“多键分组”容易让读者理解成第一种。

从结果结构看,这也是一种键到多个值的映射,常称为 multimap。multimap 描述的是数据结构,本身并不说明如何从每个元素生成多个归属键。

为什么普通 groupBy 需要先展开数据?

Object.groupBy 的回调每次为一个元素返回一个分组键,返回值会转换成对象属性键。它不会把返回的数组自动解释为多个独立的分组键。ECMAScript 规范规定了这一行为。

因此,下面的写法不能直接实现题目中的需求:

Object.groupBy(items, item => item.groups);

例如,字符串数组 ["a", "b"] 会转换为属性键 "a,b",而不会分别产生 a、b 两个分组。

题目给出的实现先把每个元素展开成若干条 [group, item] 记录,按 group 分组后再取回 item。示例数据展开后的归属关系相当于:

[a, x]
[b, x]
[a, y]
[b, z]

JavaScript 可以怎样实现?

可以将这个行为封装成独立工具,直接逐项收集,省去中间的键值对数组。下面的实现适用于示例中的字符串分组键:

const multiGroupBy = (items, callbackFn) => {
  const groups = new Map();

  for (const item of items) {
    for (const group of callbackFn(item)) {
      if (!groups.has(group)) {
        groups.set(group, []);
      }
      groups.get(group).push(item);
    }
  }

  return Object.fromEntries(groups);
};

const items = [
  { name: "x", groups: ["a", "b"] },
  { name: "y", groups: ["a"] },
  { name: "z", groups: ["b"] }
];

const result = multiGroupBy(items, item => item.groups);

console.log(result.a.map(item => item.name)); // ["x", "y"]
console.log(result.b.map(item => item.name)); // ["x", "z"]

各组保存的是原对象引用,与题目中的实现一样,不会复制对象。代码中的输出注释表示预期结果。

其他标准库有哪些对应操作?

.NET 的 System.Linq 中,SelectMany 用于展开序列,ToLookup 用于构建键到多个元素的映射,两者可以组合使用。具体语义见 Microsoft 的 SelectMany 文档和 ToLookup 文档。

对应示例可以写成:

using System;
using System.Linq;

var items = new[]
{
    new { Name = "x", Groups = new[] { "a", "b" } },
    new { Name = "y", Groups = new[] { "a" } },
    new { Name = "z", Groups = new[] { "b" } }
};

var lookup = items
    .SelectMany(
        item => item.Groups,
        (item, group) => new { Group = group, Item = item })
    .ToLookup(pair => pair.Group, pair => pair.Item);

Console.WriteLine(string.Join(", ", lookup["a"].Select(item => item.Name)));
// x, y

Console.WriteLine(string.Join(", ", lookup["b"].Select(item => item.Name)));
// x, z

ToLookup 仍然只为每条输入记录选择一个键。原始元素之所以能进入多个分组,是因为前面的 SelectMany 已经将它展开成多条归属记录。

这个例子说明标准库可以通过组合操作完成需求,但不能说明所有语言都没有专用函数。寻找某种语言或库的对应功能时,应检查回调返回的是单个键还是键集合,不能只看函数名。

封装函数时应约定哪些行为?

  • 如果回调返回空数组,该元素不会进入任何分组。
  • 如果回调返回 ["a", "a"],上面的实现和题目中的实现都会把同一元素加入 a 两次。需要每组只出现一次时,可先对归属键去重,例如返回 [...new Set(item.groups)]。
  • 示例使用字符串键。若需要用对象本身作为键,应保留 Map 作为返回值,避免 Object.fromEntries 将对象键转换为属性键。
  • 同一对象可能被多个分组共享。修改其中一个分组里的对象属性,其他分组会看到相同的修改。

备注:内容仅供参考。