JS - 分组

27 阅读7分钟

题目描述

请你编写一段可应用于所有数组的代码,使任何数组调用 array. groupBy(fn) 方法时,它返回对该数组 分组后 的结果。

数组 分组 是一个对象,其中的每个键都是 fn(arr[i]) 的输出的一个数组,该数组中含有原数组中具有该键的所有项。

提供的回调函数 fn 将接受数组中的项并返回一个字符串类型的键。

每个值列表的顺序应该与元素在数组中出现的顺序相同。任何顺序的键都是可以接受的。

请在不使用 lodash 的 _.groupBy 函数的前提下解决这个问题。

 

示例 1:

输入:
array = [
  {"id":"1"},
  {"id":"1"},
  {"id":"2"}
], 
fn = function (item) { 
  return item.id; 
}
输出:
{ 
  "1": [{"id": "1"}, {"id": "1"}],   
  "2": [{"id": "2"}] 
}
解释:
输出来自函数 array.groupBy(fn)。
分组选择方法是从数组中的每个项中获取 "id" 。
有两个 "id"1 的对象。所以将这两个对象都放在第一个数组中。
有一个 "id"2 的对象。所以该对象被放到第二个数组中。

示例 2:

输入:
array = [
  [1, 2, 3],
  [1, 3, 5],
  [1, 5, 9]
]
fn = function (list) { 
  return String(list[0]); 
}
输出:
{ 
  "1": [[1, 2, 3], [1, 3, 5], [1, 5, 9]] 
}
解释:
数组可以是任何类型的。在本例中,分组选择方法是将键定义为数组中的第一个元素。
所有数组的第一个元素都是1,所以它们被组合在一起。
{
  "1": [[1, 2, 3], [1, 3, 5], [1, 5, 9]]
}

示例 3:

输出:
array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
fn = function (n) { 
  return String(n > 5);
}
输入:
{
  "true": [6, 7, 8, 9, 10],
  "false": [1, 2, 3, 4, 5]
}
解释:
分组选择方法是根据每个数字是否大于 5 来分割数组。

题解

概述

本教程要求你将 groupBy 方法添加到所有数组中。

groupBy 操作接受一个回调函数,并返回一个新对象。此对象的键是将回调函数应用于数组中的所有项时产生的所有唯一输出。每个键都应该有一个关联的数组值。该数组应包含原始数组中回调函数返回相同键的所有值(按原始顺序排序)。

请注意,默认情况下 groupBy 方法不是内置的,因此示例代码仅在将该方法添加到 Array 原型后才能运行。你还可以在 Lodash 的常见实现中看到此方法

接下来以实际示例说明 groupBy 的工作原理:

const list = [
  { name: 'Alice', birthYear: 1990 },
  { name: 'Bob', birthYear: 1972 },
  { name: 'Jose', birthYear: 1999 },
  { name: 'Claudia', birthYear: 1974 },
  { name: 'Marcos', birthYear: 1995 }
];
const groupedByDecade = list.groupBy((person) =>  {
  const decade = Math.floor(person.birthYear / 10) * 10;
  return String(decade);
});
console.log(groupedByDecade);
/*
{
  "1990": [
    { name: 'Alice', birthYear: 1990 },
    { name: 'Jose', birthYear: 1999 },
    { name: 'Marcos', birthYear: 1995 }
  ],
  "1970": [
    { name: 'Bob', birthYear: 1972 },
    { name: 'Claudia', birthYear: 1974 }
  ]
}
*/

分组操作的用例

在前端开发和软件工程中,对列表进行分组是一种非常常见的需求。以下是一些示例用途:

构建分层树

如果要构建数据树,可以在列表上执行 groupBy 操作,然后在生成的对象的值上执行 groupBy,依此类推。这将生成一个树数据结构,可以在需要根据多个键进行高效查找的算法中进一步使用。或者该树可以用作树可视化的输入,例如可展开列表。

以下是构建此树的示例代码:

function buildTree(list, keys, index = 0) {
  if (index >= keys.length) return list;
  const group = list.groupBy((item) => item[keys[index]]);
  Object.keys(group).forEach((key) => {
    const list = group[key];
    group[key] = buildTree(list, keys, index + 1);
  });
  return group;
}

buildTree([{a: 1, b: 2}, {a: 1, b: 3}], ['a', 'b']);
/*
{
  "1": {
    "2": [{a: 1, b: 2}],
    "3": [{a: 1, b: 3}]
  }
}
*/

将两个列表上的数据合并

通常情况下,你可以同时使用多个数据列表,但有时你需要有效地将它们合并成一个列表,以供某些算法或用户界面使用。在数据库的上下文中,这会被视为连接,但在常规代码中通常需要执行合并操作。

以下示例展示了如何将十年数据与人员数据组合以创建 decadesWithPeople 变量:

const people = [
  { name: 'Alice', birthYear: 1990 },
  { name: 'Bob', birthYear: 1972 },
  { name: 'Jose', birthYear: 1999 },
  { name: 'Claudia', birthYear: 1974 },
  { name: 'Marcos', birthYear: 1995 }
];

const decades = [
  { start: 1970, theme: 'Disco' },
  { start: 1980, theme: 'Arcades' },
  { start: 1990, theme: 'Beanie Babies' }
];

const groupedByDecade = list.groupBy((person) =>  {
  const decade = Math.floor(person.birthYear / 10) * 10;
  return String(decade);
});

const decadesWithPeople = decade.map((decade) => {
  return { 
    ...decade,
    people: groupedByDecade[decade.start] || [],
  };
});

分类条形图

在创建表示某个类别的每个条的条形图之前,你需要通过类别对数据进行分组。例如,在上面的示例中,你可以使用 groupedByDecade 变量计算每个十年出生的人的平均年龄。

方法 1:使用for循环

我们可以遍历数组中的每个项目。如果返回的对象上已存在相关键,则将项目附加到相关列表。否则,将键分配给包含该项的新列表。

请注意,因为你将方法添加到Array原型,所以 this 上下文设置为数组本身。

Array.prototype.groupBy = function(fn) {
  const returnObject = {};
  for (const item of this) {
    const key = fn(item);
    if (key in returnObject) {
      returnObject[key].push(item);
    } else {
      returnObject[key] = [item];
    }
  }
  return returnObject;
};

方法 2:使用 reduce

你还可以使用 reduce 方法来解决问题。

在这里,我们将累加器值初始化为空对象 {}。对于列表中的每个项目,我们确保累加器与结果键相关联的列表。这可以通过以下代码行实现:accum[key] ||= []。该代码使用逻辑 OR 分配运算符,只在左侧参数为假时执行赋值。最后,我们将项目附加到列表并返回累加器。

Array.prototype.groupBy = function(fn) {
  return this.reduce((accum, item) => {
    const key = fn(item);
    accum[key] ||= [];
    accum[key].push(item);
    return accum;
  }, {});
};

复杂度分析:

以下分析适用于两种方法。N 代表数组的长度。理论上,这些结果可能依赖于提供的回调,但这种情况非常不常见。

时间复杂度:

  • O(N)。该算法遍历每个元素一次。

空间复杂度:

  • O(N)。该算法创建新数组,其长度总和等于原始数组的长度。

面试提示

以下是面试官可能会就这个问题提出的一些潜在问题:

  1. groupBy 方法背后的概念是什么?

    groupBy 方法是许多编程语言中的常见实用程序函数。它允许你将列表分成组,其中每个组的成员共享一个公共键。此键是通过将我们提供的函数应用于列表中的每个元素来确定的。当你希望根据某些特征对数据进行分类或聚类时,此方法非常有用。

  2. groupBy 函数的实际应用可能是什么?

    groupBy 在许多现实世界场景中都很有用,例如用于可视化数据的数据分类、组织记录以便更容易进行分析,或在将数据用于机器学习算法之前对数据进行预处理。它是一种多功能工具,可在需要根据某些共享属性或条件对项目列表进行分区时使用。

  3. 如果提供的 fn 函数不返回字符串会发生什么?

    fn 函数应返回一个字符串,然后将其用作结果对象中的键。如果fn返回非字符串值,JavaScript 将隐式将此值转换为字符串以将其用作对象键。但这可能不是预期的行为,可能会导致意外的结果。因此,重要的是确保 fn 始终返回一个字符串。

  4. 如何处理空数组的情况?

    如果输入数组为空,for 循环和 reduce 实现都将返回一个空对象。这是因为没有要分组的元素,因此结果对象中没有要添加的键。

  5. 如何实现允许在回调函数中进行异步操作的 groupBy 函数?

    这个问题需要理解 JavaScript 的异步性质以及使用 Promise 的方法。你可以通过学习 Promise 的工作原理以及如何使用async/await语法来准备。考虑 groupBy 函数的结构如何更改以适应异步操作。

  6. 如何使用 groupBy 来帮助理解应用程序中的错误。

    每个错误都有与之关联的类型。通过 groupBy,你可以确定哪种类型的错误更常见。你还可以分析调用堆栈以确定错误的原始文件。