【力扣】2705. 精简对象
文章目录
- [【力扣】2705. 精简对象](#【力扣】2705. 精简对象)
题目
现给定一个对象或数组 obj,返回一个 精简对象 。
精简对象 与原始对象相同,只是将包含 假 值的键移除。该操作适用于对象及其嵌套对象。数组被视为索引作为键的对象。当 Boolean(value) 返回 false 时,值被视为 假 值。
你可以假设 obj 是 JSON.parse 的输出结果。换句话说,它是有效的 JSON。
示例 1:
输入:obj = [null, 0, false, 1]
输出:[1]
解释:数组中的所有假值已被移除。
示例 2:
输入:obj = {"a": null, "b": [false, 1]}
输出:{"b": [1]}
解释:obj["a"] 和 obj["b"][0] 包含假值,因此被移除。
示例 3:
输入:obj = [null, 0, 5, [0], [false, 16]]
输出:[5, [], [16]]
解释:obj[0], obj[1], obj[3][0], 和 obj[4][0] 包含假值,因此被移除。
提示:
obj是一个有效的 JSON 对象2 <= JSON.stringify(obj).length <= 106
解决方案
概述
在这个问题中,你需要实现一个 JavaScript 函数 compactObject,该函数接收一个 JSON 对象作为输入,并返回对象的"紧凑"版本。所谓"紧凑"指的是原始对象,但将所有与假值关联的键都移除。这个移除过程不仅适用于顶层对象,还适用于任何嵌套的对象或数组。在这个问题的上下文中,数组被视为具有索引作为键的对象。当 Boolean(value) 返回 false 时,值被视为假值。
为了说明该函数的行为,提供了三个示例。第一个示例演示了如何移除数组中的所有假值。第二个示例显示由于假值而移除对象中的键值对。第三个示例稍微复杂,演示了在涉及对象和数组的嵌套结构中移除假值的操作。
要掌握这个问题,需要对 JavaScript 中的对象和数组操作有坚实的掌握,尤其是在迭代和修改它们的内容方面。此外,理解 JavaScript 中的假值是至关重要的。
处理假值
在 JavaScript 中,如果在布尔上下文中评估时值转换为 false,则该值被视为假值。这包括 false、0、-0、'' 和 ""(空字符串)、null、undefined 和 NaN。
在我们的问题中,我们希望移除具有假值的键。为了实现这一点,在我们的函数中使用了 if(!obj) return false; 检查。这个简洁的检查有效地捕捉到所有的假值,使我们能够在输出中忽略它们。
这个条件还可以正确处理 null。尽管由于历史原因,在 JavaScript 中 typeof null 返回 object,但 null 确实是一个假值。它是一个代表任何对象值的缺失的原始值。因此,在我们的上下文中,具有 null 值的键也将被忽略。
compactObject 的应用场景
compactObject 函数在涉及处理和操作 JSON 数据的 JavaScript 应用程序中可以是一个强大的工具。它的主要功能是清除具有假值的键的对象(或数组,在 JavaScript 中被视为具有索引作为键的对象),包括嵌套的键。以下是一些通用领域,这些功能可能有用:
- 数据清洗:在许多现实世界应用程序中,数据经常以不同格式从各种来源获取,有时包含不必要的键或具有假值的键。使用
compactObject可以在进一步处理之前清理这些数据。例如,如果我们有一个嵌套对象,如 var obj = { key1: "", key2: { key3: null, key4: "value" }},compactObject将返回 { key2: { key4: "value" }},有效地消除了空值或 null 值。 - API 响应处理:在与第三方 API 的响应一起工作时,发现具有假值的键并不是不常见的,如果不妥善处理可能会导致问题。
compactObject可用于删除这些键,确保 API 响应更清洁,对于后续操作更可预测。 - UI 渲染:在将数据渲染到用户界面之前,移除任何具有假值的键可以创建更清洁的用户界面。例如,考虑一个 UI 组件,需要一个对象来显示用户配置文件。如果对象包括具有 null 或 undefined 值的字段,可能会导致 UI 中出现空白或错误。通过使用
compactObject,我们可以在将对象传递给 UI 组件之前删除这些字段。 - 优化存储:在存储数据时,使用
compactObject来删除具有假值的键可以帮助优化存储利用率,确保只保存有意义的数据。
需要注意的是,compactObject 的使用高度依赖于应用程序的特定要求和上下文。可能存在保留具有假值的键是必要的情况。因此,在决定应用此函数之前,请始终考虑你的具体用例。
方法 1:递归深度优先搜索(DFS)
概述
在这种方法中,我们使用递归的深度优先搜索(DFS)
的概念。主要思想是递归地遍历对象的深度并重建对象或数组,而不包含任何假值。
算法
- 基本情况:如果当前值是假值,则返回 false。如果当前值不是对象,则返回该值。
- 处理数组:如果当前值是数组,我们遍历数组并递归处理每个项。如果递归调用的返回值为真,我们将其添加到一个新数组中。
- 处理对象:如果当前值是对象,我们遍历对象的键,并递归处理每个值。如果递归调用的返回值为真,我们将其添加到一个新对象中。
- 返回结果:最后,我们返回清理后的对象或数组。
实现
js
function compactObject(obj) {
function dfs(obj) {
if(!obj) return false;
if(typeof obj !== 'object') return obj;
if(Array.isArray(obj)) {
const newArr = [];
for(let i = 0; i < obj.length; i++) {
const curr = obj[i];
const subRes = dfs(curr);
if(subRes) {
newArr.push(subRes);
}
}
return newArr;
}
const newObj = {};
for(const key in obj) {
const subRes = dfs(obj[key]);
if(subRes) {
newObj[key] = subRes;
}
}
return newObj;
}
return dfs(obj);
}
这个实现利用递归来遍历和清理输入对象或数组。它根据 JavaScript 中对象的独特特性区分处理对象和数组。如果对子项进行 dfs 函数调用的返回值为真,那么该子项将被添加到新对象或数组中。因此,所有假值(包括空对象或数组)都被有效移除。
复杂度分析
- 时间复杂度:O(N),其中 N 是对象中的元素总数(包括嵌套元素)。该函数仅对对象中的每个元素进行一次遍历,包括遍历每个嵌套对象或数组。因此,时间复杂度与元素总数成正比。
- 空间复杂度:O(D),其中 D 是对象的深度。在递归调用期间,堆栈调用使用了额外的空间。在最坏的情况下,递归的深度等于对象的深度,因此空间复杂度与对象的深度成正比。这假设对象键和数组元素不计入空间复杂度,因为它们是输入的一部分。如果包括它们,空间复杂度可能被视为 O(N),与时间复杂度类似。
方法 2:迭代深度优先搜索
思路
在处理嵌套对象时,我们可能会选择使用递归,因为递归简单而优雅。然而,递归也伴随着一系列挑战,例如在处理大型输入时可能导致堆栈溢出。因此,在这种情况下,使用手动管理的堆栈的迭代方法可能更有利。
算法
- 初始化一个堆栈数据结构,将输入对象添加到堆栈中。同时,创建一个新对象,它将在遍历原始对象时被填充。
- 迭代深度探索:只要堆栈上仍然有对象,就从堆栈中弹出一个对象。对于对象中的每个键值对,检查值是否为对象或数组。如果值是对象或数组,用新的空对象或数组替换副本中的相应值,并将该值添加到堆栈中。
- 守卫语句:在迭代期间,我们忽略值为假值的键值对(或数组中的索引)。
- 最终输出:一旦堆栈为空,表示我们已经探索了输入中的所有对象和数组。此时,我们的副本已被修改,只包含具有真值的键(或数组中的索引),因此我们返回它。
实现
js
function compactObject(obj) {
const stack = [[obj, Array.isArray(obj) ? [] : {}]];
let newObj = stack[0][1];
while(stack.length > 0) {
const [currObj, newCurrObj] = stack.pop();
for(const key in currObj) {
const val = currObj[key];
if(!val) continue;
if(typeof val !== 'object') {
Array.isArray(newCurrObj) ? newCurrObj.push(val) : newCurrObj[key] = val;
continue;
}
const newSubObj = Array.isArray(val) ? [] : {};
Array.isArray(newCurrObj) ? newCurrObj.push(newSubObj) : newCurrObj[key] = newSubObj;
stack.push([val, newSubObj]);
}
}
return newObj;
}
这个实现使用一个显式的堆栈来管理 DFS,以遍历输入对象。这意味着它能够处理更大的输入,取决于可用的堆内存。
复杂度分析
- 时间复杂度:O(N),其中 N 是输入对象和所有嵌套对象/数组中的键(或索引)的总数。这是因为我们只处理每个键或索引一次。
- 空间复杂度:O(N),与上面的相同。主要用于堆栈,堆栈在最坏的情况下同时存储了所有嵌套的对象和数组。此外,还有一些用于输入对象的副本的额外空间,但这不会改变总的 O(N) 空间复杂度。
面试提示
- 你能解释
compactObject函数在递归方法中的工作原理吗?- 递归方法中的递归调用的返回值为真时,会将该子项包含在新对象或数组中。因此,任何假值(例如空对象或数组)都被移除。
- 迭代方法与递归方法之间的主要区别是什么?
- 递归方法使用系统调用堆栈,因此所使用的空间与对象的深度成正比。这可能导致在深度嵌套对象的情况下堆栈溢出。另一方面,迭代方法使用显式堆栈来管理 DFS,可以处理更大的输入,取决于可用的堆内存。
- 你能解释为什么我们需要在两种方法中都检查值是否为对象或数组吗?
- 检查值是否为对象或数组是因为 JavaScript 将数组视为对象的一种类型。然而,JavaScript 中数组和非数组对象的语义不同 - 具体来说,它们的键处理方式不同。在数组中,键是索引,顺序很重要,而在非数组对象中,键是字符串,顺序不重要。因此,必须分别处理这两种情况以适应
compactObject函数。
- 检查值是否为对象或数组是因为 JavaScript 将数组视为对象的一种类型。然而,JavaScript 中数组和非数组对象的语义不同 - 具体来说,它们的键处理方式不同。在数组中,键是索引,顺序很重要,而在非数组对象中,键是字符串,顺序不重要。因此,必须分别处理这两种情况以适应
- 从时间和空间复杂度的角度看,递归和迭代方法之间的权衡是什么?
- 递归方法和迭代方法在时间复杂度方面非常相似 - 它们需要一次访问每个键或索引,因此具有线性时间复杂度 O(N)。然而,它们在空间复杂度上有所不同。递归方法使用系统调用堆栈,因此所使用的空间与对象的深度有关。这可能导致在深度嵌套对象的情况下堆栈溢出。另一方面,迭代方法使用显式堆栈来管理 DFS,可以处理更大的输入,取决于可用的堆内存。