ES6的fill和for循环创建数组的区别

215 阅读3分钟

在做一道leetcode题目时,使用了fill方法创建数组始终无法AC,但是for循环就可以,于是看了一眼fill方法的官方文档找到了原因。

题目如下:

221. 最大正方形

在一个由 0 和 1 组成的二维矩阵内,找到只包含 1 的最大正方形,并返回其面积。

测试用例:

输入: 

1 0 1 0 
1 0 1 1 
1 0 1 1
1 1 1 1

输出: 4

这是一道典型的dp题,我的解答如下:

var maximalSquare = function(matrix) {
    if (!matrix.length) return 0
    let n = matrix.length, m = matrix[0].length, ans = 0
    const dp = Array(n).fill([])                        // 在该处使用了fill方法
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < m; j++) {
            if (matrix[i][j] === '1') {
                dp[i][j] = Math.min(i && j ? dp[i - 1][j - 1] : 0, j ? dp[i][j - 1] : 0, i ? dp[i - 1][j] : 0) + 1
                ans = Math.max(dp[i][j], ans)
            } else dp[i][j] = 0    
        }
    }
    return ans * ans
}

运行以上代码得到结果为9,并不是4。

把fill方法那一行换成for循环:

......
const dp = []
for (let i = 0; i < n; i++) dp[i] = []
......

结果为4,符合预期。

为了找到fill方法出错的位置,我打印了每次循环后的dp[i][j]:

var maximalSquare = function(matrix) {
    if (!matrix.length) return 0
    let n = matrix.length, m = matrix[0].length, ans = 0
    const dp = Array(n).fill([])                        // 在该处使用了fill方法

    for (let i = 0; i < n; i++) {
        for (let j = 0; j < m; j++) {
            if (matrix[i][j] === '1') {
                dp[i][j] = Math.min(i && j ? dp[i - 1][j - 1] : 0, j ? dp[i][j - 1] : 0, i ? dp[i - 1][j] : 0) + 1
                ans = Math.max(dp[i][j], ans)
            } else dp[i][j] = 0
            console.log(`i = ${i}, j = ${j}, dp[i][j] = ${dp[i][j]}`)    // 输出每次循环赋值后的dp[i][j]大小
        }
    }
    return ans * ans
}

输出如下:

i = 0, j = 0, dp[i][j] = 1
i = 0, j = 1, dp[i][j] = 0
i = 0, j = 2, dp[i][j] = 1
i = 0, j = 3, dp[i][j] = 0
i = 1, j = 0, dp[i][j] = 1
i = 1, j = 1, dp[i][j] = 0
i = 1, j = 2, dp[i][j] = 1
i = 1, j = 3, dp[i][j] = 1
i = 2, j = 0, dp[i][j] = 1
i = 2, j = 1, dp[i][j] = 0
i = 2, j = 2, dp[i][j] = 1
i = 2, j = 3, dp[i][j] = 2
i = 3, j = 0, dp[i][j] = 1
i = 3, j = 1, dp[i][j] = 1                       
i = 3, j = 2, dp[i][j] = 2                      // dp[3][2]应该为1
i = 3, j = 3, dp[i][j] = 3

可以看到dp[3][2]输出为2,而不是想要的1。于是我又打印了i = 3, j = 2时dp[i][j]的生成过程。

var maximalSquare = function(matrix) {
    if (!matrix.length) return 0
    let n = matrix.length, m = matrix[0].length, ans = 0
    const dp = Array(n).fill([])                        // 在该处使用了fill方法

    for (let i = 0; i < n; i++) {
        for (let j = 0; j < m; j++) {
            if (matrix[i][j] === '1') {
                if (i === 3 && j === 2) {       // 输出用来生成dp[i][j]的几个dp参数
                    console.log(`dp[i - 1][j - 1] = ${dp[i - 1][j - 1]}`)
                    console.log(`dp[i][j - 1] = ${dp[i][j - 1]}`)
                    console.log(`dp[i - 1][j] = ${dp[i - 1][j]}`)
                }
                dp[i][j] = Math.min(i && j ? dp[i - 1][j - 1] : 0, j ? dp[i][j - 1] : 0, i ? dp[i - 1][j] : 0) + 1
                ans = Math.max(dp[i][j], ans)
            } else dp[i][j] = 0
            console.log(`i = ${i}, j = ${j}, dp[i][j] = ${dp[i][j]}`)    // 输出每次循环赋值后的dp[i][j]大小
        }
    }
    return ans * ans
}

输出如下:

i = 0, j = 0, dp[i][j] = 1
i = 0, j = 1, dp[i][j] = 0
i = 0, j = 2, dp[i][j] = 1
i = 0, j = 3, dp[i][j] = 0
i = 1, j = 0, dp[i][j] = 1
i = 1, j = 1, dp[i][j] = 0
i = 1, j = 2, dp[i][j] = 1
i = 1, j = 3, dp[i][j] = 1
i = 2, j = 0, dp[i][j] = 1
i = 2, j = 1, dp[i][j] = 0                       // 1
i = 2, j = 2, dp[i][j] = 1
i = 2, j = 3, dp[i][j] = 2
i = 3, j = 0, dp[i][j] = 1
i = 3, j = 1, dp[i][j] = 1                       // 3
dp[i - 1][j - 1] = 1                             // 2                        
dp[i][j - 1] = 1
dp[i - 1][j] = 1
i = 3, j = 2, dp[i][j] = 2
i = 3, j = 3, dp[i][j] = 3

可以看到行2,当i = 3, j = 2时,dp[i - 1][j - 1]即dp[2][1]变成了1,而不是行1的0,这是为什么呢?原因是行3给dp[3][1]赋值1的时候覆盖了行1中dp[2][1]的值。下面我来详细解释一下。

我查了一下MDN的Array.prototypes.fill方法,有这么一句话,

When fill gets passed an object, it will copy the reference and fill the array with references to that object.

也就是说,如果填充的类型为对象(包括数组),那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。示例如下:

// 官方示例
// Objects by reference.
var arr = Array(3).fill({}) // [{}, {}, {}];
arr[0].hi = "hi"; // [{ hi: "hi" }, { hi: "hi" }, { hi: "hi" }]
// 数组同理
let arr = new Array(5).fill([])
arr[1].push(6)
arr // [[6], [6], [6], [6], [6]]

回到上面的题目,也就是i = 3, j = 1时,给dp[3][1]赋值为1会使dp[0][1]、dp[1][1]、dp[2][1]全变成1,导致最终结果错误。

总结

创建一个由对象(包括数组)组成的数组时,如果该数组中的元素还会改变,不要使用fill方法,老实用for循环。因为该方法只会给数组填充同一个内存地址的对象,而不是深拷贝对象。