在做一道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循环。因为该方法只会给数组填充同一个内存地址的对象,而不是深拷贝对象。