【力扣】2625. 扁平化嵌套数组
文章目录
- [【力扣】2625. 扁平化嵌套数组](#【力扣】2625. 扁平化嵌套数组)
题目
请你编写一个函数,它接收一个 多维数组 arr 和它的深度 n ,并返回该数组的 扁平化 后的结果。
多维数组 是一种包含整数或其他 多维数组 的递归数据结构。
数组 扁平化 是对数组的一种操作,定义是将原数组部分或全部子数组删除,并替换为该子数组中的实际元素。只有当嵌套的数组深度小于 n 时,才应该执行扁平化操作。第一层数组中元素的深度被认为是 0。
请在没有使用内置方法 Array.flat 的前提下解决这个问题。
示例 1:
输入
arr = [1, 2, 3, [4, 5, 6], [7, 8, [9, 10, 11], 12], [13, 14, 15]]
n = 0
输出
[1, 2, 3, [4, 5, 6], [7, 8, [9, 10, 11], 12], [13, 14, 15]]
解释
传递深度 n=0 的多维数组将始终得到原始数组。这是因为子数组(0)的最小可能的深度不小于 n = 0。因此,任何子数组都不应该被平面化。
示例 2:
输入
arr = [1, 2, 3, [4, 5, 6], [7, 8, [9, 10, 11], 12], [13, 14, 15]]
n = 1
输出
[1, 2, 3, 4, 5, 6, 7, 8, [9, 10, 11], 12, 13, 14, 15]
解释
以 4 、7 和 13 开头的子数组都被扁平化了,这是因为它们的深度为 0, 而 0 小于 1。然而 [9,10,11] 其深度为 1,所以未被扁平化。
示例 3:
输入
arr = [[1, 2, 3], [4, 5, 6], [7, 8, [9, 10, 11], 12], [13, 14, 15]]
n = 2
输出
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
解释
所有子数组的最大深度都为 1。因此,它们都被扁平化了。
提示:
0 <= arr 的元素个数 <= 1050 <= arr 的子数组个数 <= 105maxDepth <= 1000-1000 <= each number <= 10000 <= n <= 1000
解决方案
概述
你将获得一个多维数组arr和一个整数 n。你的任务是通过移除数组 arr 中深度为 n 的子数组来扁平化数组。扁平化后的数组应该只包含来自子数组的实际元素,而不包含子数组本身。
用例
数据处理
- 在处理嵌套格式的数据时,比如 JSON 或 XML,扁平化结构可以简化数据处理任务。例如,如果你从 API 中获得了嵌套的 JSON 响应,但只需要某些字段,扁平化 JSON 可以更容易地提取和分析所需的数据。
- 例如:
js
const people = [
{ name: 'Mike', items: ['hammer', 'axe'] },
{ name: 'Steve', items: ['rock', 'stick'] }
];
const allItems = people.map(d => d.items).flat();
树遍历
- 在计算机科学中,树经常用于表示层次结构。当遍历类似树状结构时,扁平化可用于将树转换为线性表示,从而更容易导航和操作数据。
递归算法
- 许多算法涉及数据结构的递归操作。在这些情况下,扁平化嵌套数组可能很有帮助。例如,在实现深度优先搜索或递归回溯等算法时,通过提供数据的线性视图,扁平化可以简化处理过程。
数据库查询
- 在数据库系统中,嵌套结构可以存储为数组或 JSON 对象。在查询数据时,扁平化嵌套数组可以用于检索特定元素或在结构的不同层次上执行聚合。
方法 1:递归方法
概述
由于这是一个深度嵌套结构,解决方法中将涉及一些重复步骤,因此递归是解决此问题的合适方法。
算法
- 我们创建一个递归函数 flattening,该函数接受数组
nums和层级 l 作为参数。 - 在 flattening 中,我们使用 for...of 循环来迭代
nums数组的元素。 - 对于每个元素,我们检查它是否是数组以及层级 l 是否在指定范围内(l > 0 且 l <= n)。
- 如果元素是数组且满足层级条件,我们会递归调用 flattening,并将嵌套数组和层级减小 1(即 l - 1)传递给它。
- 如果元素不是数组或层级条件不满足,我们将元素推送到结果数组 res 中。
- 最初,我们使用数组
arr和深度 n 作为参数调用 flattening 函数以启动扁平化过程。 - 最后,将包含扁平化元素的 res 数组作为结果返回。
实现
js
/**
* @param {any[]} arr
* @param {number} depth
* @return {any[]}
*/
var flat = function (arr, n) {
let res = [];
const flattening = (nums, l) => {
for (const num of nums) {
if (Array.isArray(num) && l > 0) {
flattening(num, l - 1);
} else {
res.push(num);
}
}
}
flattening(arr, n);
return res;
};
复杂度分析:
- 时间复杂度:
- 最坏情况下,每个元素都是一个数组,并且最大深度 n 被达到,递归函数将在每个层级的每个嵌套数组上被调用。
- 这导致总共 O(k∗n) 次递归调用,其中 k 表示每个层级平均嵌套数组的数量,n 是最大深度级别。
- 空间复杂度:
- 解决方案的空间复杂度由深度级别 n 决定,因为它影响递归的最大深度。
- 每个递归调用在调用堆栈上消耗额外的空间,因此空间复杂度为 O(n),因为我们需要为调用堆栈上的 n 次递归调用分配空间。
方法 2:使用迭代队列
概述
我们可以将嵌套数组的所有子数组放入队列以便在后续迭代中处理。
算法步骤
- 我们首先初始化一个布尔变量
nestedArrayElement为 true,表示仍然有嵌套数组需要扁平化。 - 我们还初始化一个变量 queue,用于在扁平化过程中存储元素。
- 初始时,将深度 depth 设置为 0,表示当前深度级别。
- 函数进入一个 while 循环,只要还有单个数组元素需要处理
(nestedArrayElement 为 true),且深度小于 n。- 在每次循环迭代内,首先将
nestedArrayElement设置为 false,表示尚未遇到嵌套数组。 - 为当前迭代创建一个新的空队列 queue,用于存储当前迭代中的元素。
- 使用 for 循环遍历数组
arr中的每个元素。- 如果元素是数组,将其元素展开到队列 queue 中,并将
nestedArrayElement设置为 true,表示遇到嵌套数组。 - 如果元素不是数组,将其直接推送到队列 queue 中。
- 如果元素是数组,将其元素展开到队列 queue 中,并将
- 处理完数组
arr的所有元素后,将数组arr更新为队列中的元素。 - 这个操作通过扩展操作
arr = [...queue]实现,有效地更新了数组arr为当前级别的嵌套数组的扁平化元素。这个更新后的数组arr将在下一次 while 循环迭代中用于扁平化下一个级别的嵌套数组。 - 增加深度 depth,表示处理了新的嵌套级别。
- 在每次循环迭代内,首先将
- 循环继续,直到没有更多单个数组元素(
nestedArrayElement 为 false)或深度达到指定级别 n。 - 最后,将更新后的数组
arr作为扁平化后的数组返回。
实现
js
/**
* @param {any[]} arr
* @param {number} depth
* @return {any[]}
*/
var flat = function (arr, n) {
let nestedArrayElement = true;
let queue;
let depth = 0;
while(nestedArrayElement && depth < n) {
nestedArrayElement = false;
queue = [];
for(let i = 0; i < arr.length; i++) {
if(Array.isArray(arr[i])) {
queue.push(...arr[i]);
nestedArrayElement = true;
} else {
queue.push(arr[i]);
}
}
arr = [...queue];
depth++;
}
return arr;
};
复杂度分析:
-
时间复杂度:
- 时间复杂度为 O(n∗m),其中 n 是最大深度级别,m 是数组中的总元素数量。我们为每个级别中的所有元素迭代,所以总复杂度为 O(n∗m)。
-
空间复杂度:
- 空间复杂度为 O(m),因为我们使用了额外的队列来存储数组中的元素。
方法 3:使用迭代堆栈
概述
我们可以将输入数组的每个元素与初始深度级别 n 映射到一个堆栈中。然后,在 while 循环中,我们弹出堆栈中的元素和深度。如果元素是数组且深度大于 0,则将数组的每个元素与降低的深度推回堆栈中。这个过程一直进行,直到堆栈为空。如果元素不是数组或深度为 0,则它被视为叶元素,并将其推送到结果数组中。最后,结果数组被反转以保持元素的原始顺序。
算法
- 我们初始化一个名为 stack 的堆栈,其中包含数组 arr 中的元素以及它们的初始深度级别。每个元素表示为一个对 [item, depth]。
- 我们还初始化一个空数组 res,用于存储扁平化的结果。
- 函数进入一个 while 循环,只要堆栈中仍有元素。
- 在每次循环迭代中,使用 pop() 方法从堆栈中获取顶部元素 [item, depth]。
- 如果元素是数组且当前深度 depth 大于 0,表示元素是包含需要进一步扁平化的项的盒子。
- 将元素 item 数组中的每个元素映射到对 [el, depth - 1],表示元素以及降低的深度级别(depth - 1)。
- 这些映射的对将使用 push() 方法重新推回堆栈,有效地排队以供后续处理。
- 如果元素不是数组或深度为 0,表示元素是一个项,它可以直接添加到结果数组 res 中。
- 循环继续,直到堆栈中没有更多元素。
- 最后,将包含扁平化元素的结果数组 res 返回,但以相反的顺序。为了确保元素保持原始顺序,使用 reverse() 方法进行反转。
实现
js
/**
* @param {any[]} arr
* @param {number} depth
* @return {any[]}
*/
var flat = function (arr, n) {
const stack = [...arr.map((item) => [item, n])];
const res = [];
while (stack.length > 0) {
const [item, depth] = stack.pop();
if (Array.isArray(item) && depth > 0) {
// 降低深度后重新推回堆栈
stack.push(...item.map((el) => [el, depth - 1]));
} else {
res.push(item);
}
}
return res.reverse();
};
复杂度分析:
- 时间复杂度:
- 代码涉及到一个 while 循环,只要堆栈中有元素,每次迭代中弹出元素并进行处理。
- 在最坏情况下,每个元素仅处理一次,需要迭代输入数组的元素并映射它们以推回堆栈。
- 因此,代码的时间复杂度是 O(m),其中 m 是输入数组中的总元素数量。
- 空间复杂度:
- 代码的空间复杂度是 O(m),其中 m 是输入数组中的总元素数量。
面试提示
- 解释扁平化多维数组的概念。为什么在某些情景下扁平化很有用?
扁平化多维数组意味着通过删除任何嵌套数组并替换为它们的实际元素将其转换为单维数组。这在需要将数组作为扁平列表进行处理、而不考虑其原始嵌套结构时很有用。它简化了搜索、过滤或转换数组元素的操作。
- 是否存在需要考虑的特殊情况或边缘情景?你的解决方案如何处理这些情况?
是的,我们应该考虑空数组或没有嵌套数组的情况。在这种情况下,函数应该返回原始数组,因为没有需要扁平化的嵌套数组。此外,我们需要处理深度 n 为负数或超出数组的实际深度的情况。在这些情况下,函数也应返回原始数组,而不进行扁平化。
- 你的解决方案如何处理输入数组中的循环引用或自引用数组?
循环引用或自引用数组可能导致无限递归。提供的解决方案不显式处理循环引用。如果输入数组包含循环引用,递归扁平化过程可能导致无限循环或堆栈溢出错误。