数组
数组是将 相同类型 的元素存储于 连续内存空间 的数据结构,其长度不可变;
访问/修改-索引 O(1)
插入元素 O(n)
删除元素 O(n)
合并数组 O(m+n)
数组的特殊性
-
JS 数组可以动态的改变容量,根据元素的数量来扩容、收缩;
-
JS 数组可以表现的像栈一样,为数组提供了 push 和 pop 方法;也可以表现的像队列一样,使用 shift 和 push 方法,可以像使用队列一样使用数组;
-
JS 数组可以使用 for-each 遍历,可以排序,可以倒置;
-
JS 提供了很多操作数组的方法,比如 Array.concat、Array.join、Array.slice;
快慢数组
-
快数组是一种 线性 的存储方式,新创建的空数组,默认的存储方式是快数组,快数组长度是可变的,可以根据元素的增加和删除来动态调整存储空间大小,内部是通过扩容和收缩机制实现;
扩容后的新容量 = 旧容量的 1.5 倍 + 16- 需要根据
length + 1 == old_length进行判断,true 则将空出的空间收缩二分之一,false 则全部收缩掉;
-
慢数组是一种 字典 的内存形式,不用开辟大块连续的存储空间,节省了内存,但是由于需要维护这样一个 HashTable 其效率会比快数组低;
前端实战:如何避免触发慢数组(性能优化)
-
避免创建稀疏数组:
// ❌ 错误:创建稀疏数组,触发慢数组 const badArr = []; badArr[1000] = 'hello'; // ✅ 正确:如果需要固定长度数组,先填充默认值(保持密集) const goodArr = new Array(1001).fill(undefined); goodArr[1000] = 'hello'; // 仍为快数组 -
避免用 delete 删除数组元素:
const arr = [1,2,3,4]; // ❌ 错误:delete 留下空洞,可能触发慢数组 delete arr[1]; // arr 变为 [1,,3,4] // ✅ 正确:用 splice 彻底删除,保持密集 arr.splice(1, 1); // arr 变为 [1,3,4](仍为快数组) -
大数据数组初始化优化:
// ❌ 错误:频繁 push 导致快数组多次扩容(虽不触发慢数组,但有性能损耗) const bigArr = []; for (let i = 0; i < 100000; i++) { bigArr.push(i); } // ✅ 正确:提前初始化长度,减少扩容次数 const bigArr = new Array(100000); for (let i = 0; i < 100000; i++) { bigArr[i] = i; }
前端业务中的实际影响
-
普通业务场景:日常的列表数据 (长度 < 1000),快 / 慢数组的性能差异几乎感知不到,无需过度关注;
-
大数据场景:处理 10万+ 元素的数组 (如数据可视化、大数据导出),慢数组会导致遍历 / 访问性能下降 (约 20%-50%),需避免触发;
-
框架层面:React/Vue 等框架的列表渲染底层会规避稀疏数组 (比如 v-for 遍历前先过滤空洞),但自定义大数据处理时仍需注意;
二维矩阵与一维索引互转公式
-
行优先遍历 = “先走完一行的所有列,再走下一行”,就像我们读书时 “从左到右、从上到下” 的阅读顺序;
-
可视化示例:3 行 4 列矩阵 (行索引 i:0/1/2;列索引 j:0/1/2/3):
行0:[ (0,0), (0,1), (0,2), (0,3) ] → 元素顺序:1, 2, 3, 4 行1:[ (1,0), (1,1), (1,2), (1,3) ] → 元素顺序:5, 6, 7, 8 行2:[ (2,0), (2,1), (2,2), (2,3) ] → 元素顺序:9, 10, 11, 12 -
行优先遍历的一维顺序:1→2→3→4→5→6→7→8→9→10→11→12;
公式 1:二维索引 → 一维索引((i,j) → k)
-
公式:
一维索引k = 行索引i × 矩阵列数n + 列索引j; -
核心逻辑,把二维矩阵想象成 “排队”:
- 行索引
i = 你前面有多少 “完整的队伍(行)”; - 列索引
j = 你在自己队伍里的位置; - 一维索引
k = 前面所有队伍的人数 + 你在自己队伍里的位置;
- 行索引
-
可视化对应表:
二维索引 (i,j) 元素 计算 (i×n+j) 一维索引 k (0,0) 1 0×4+0=0 0 (0,1) 2 0×4+1=1 1 (0,2) 3 0×4+2=2 2 (0,3) 4 0×4+3=3 3 (1,0) 5 1×4+0=4 4 (1,1) 6 1×4+1=5 5 (1,2) 7 1×4+2=6 6 (1,3) 8 1×4+3=7 7 (2,0) 9 2×4+0=8 8
公式 2:一维索引 → 二维索引(k → (i,j))
-
这是 公式 1 的逆运算,核心是
“除法求行、取模求列”; -
公式:
- 行索引
i = Math.floor(一维索引 k / 矩阵列数 n); - 列索引
j = 一维索引 k % 矩阵列数 n;
- 行索引
-
核心逻辑,把一维数组想象成 “拆分成若干行”:
- 除法
Math.floor(k/n)= 能拆出多少个 “完整的行” (即行索引); - 取模
k % n= 拆分完完整行后,剩下的元素数 (即列索引);
- 除法
-
可视化对应表 (反向推导):
一维索引 k 元素 计算行 (i=Math.floor (k/n)) 计算列 (j=k% n) 二维索引 (i,j) 6 7 Math.floor(6/4)=1 6%4=2 (1,2) 7 8 Math.floor(7/4)=1 7%4=3 (1,3) 8 9 Math.floor(8/4)=2 8%4=0 (2,0) 9 10 Math.floor(9/4)=2 9%4=1 (2,1)
力扣题单
重塑矩阵
-
实现方式一:
- 核心思路:将二维矩阵 “扁平化” 为一维数组,再按新的行列数重新分割为二维数组;
- 步骤拆解:
- 计算原矩阵的总行数 rows、总列数 cols,以及总元素数
total = rows * cols; - 校验 total 是否等于 r*c:若不等,直接返回原矩阵;
- 若相等,先将原矩阵扁平化为一维数组 (按行遍历);
- 按 r 行 c 列的规则,将一维数组重新分割为二维数组;
- 计算原矩阵的总行数 rows、总列数 cols,以及总元素数
- 复杂度:
- 时间复杂度:O(rc);
- 空间复杂度:O(mn),这里的空间复杂度不包含返回的重塑矩阵需要的空间;
- 实现代码:
function matrixReshape(nums: number[][], r: number, c: number): number[][] { // 1. 获取原矩阵的行数和列数(处理空矩阵边界情况) const rows = nums.length; if (rows === 0) return nums; // 空矩阵直接返回 const cols = nums[0].length; const total = rows * cols; // 2. 校验元素总数是否匹配,不匹配则返回原矩阵 if (total !== r * c) { return nums; } // 3. 扁平化原矩阵为一维数组(行优先遍历) const flatNums: number[] = []; for (let i = 0; i < rows; i++) { for (let j = 0; j < cols; j++) { flatNums.push(nums[i][j]); } } // 4. 重塑为 r 行 c 列的新矩阵 const result: number[][] = []; let index = 0; // 指向flatNums的指针 for (let i = 0; i < r; i++) { const row: number[] = []; for (let j = 0; j < c; j++) { row.push(flatNums[index]); index++; } result.push(row); } return result; } -
实现方式二:
- 公式 2:一维索引 → 二维索引(k → (i,j)):
- 行索引
i = Math.floor(一维索引 k / 矩阵列数 n); - 列索引
j = 一维索引 k % 矩阵列数 n;
- 行索引
- 复杂度:
- 时间复杂度:O(rc);
- 空间复杂度:O(1);
- 实现代码:
function matrixReshape(nums: number[][], r: number, c: number): number[][] { // 获取原矩阵行数,处理空矩阵边界 const row = nums.length; if (row === 0) return nums; // 获取原矩阵列数(非空矩阵至少有一行) const col = nums[0].length; // 校验元素总数是否匹配,不匹配则返回原矩阵 if (row * col !== r * c) return nums; // 初始化 r 行 c 列的结果矩阵,避免数组引用共享问题 const ans: number[][] = new Array(r).fill(0).map(() => new Array(c).fill(0)); // 遍历所有元素的一维索引,通过数学公式映射到原矩阵和新矩阵的二维位置 for (let i = 0; i < row * col; ++i) { ans[Math.floor(i / c)][i % c] = nums[Math.floor(i / col)][i % col]; } return ans; } - 公式 2:一维索引 → 二维索引(k → (i,j)):
矩阵置零
-
实现方式一:暴力法 (额外空间记录零的行 / 列)
- 思路:
- 遍历矩阵,用两个集合分别记录包含 0 的行号和列号,用 Set 避免重复记录同一行 / 列 (比如一行有多个 0 只需记录一次);
- 再次遍历矩阵,若当前位置的行 / 列在记录集合中,则置为 0;
- 优点:逻辑简单易懂,适合新手;缺点:需要额外空间存储行 / 列信息;
- 复杂度:
- 时间复杂度:O(mn) (两次遍历矩阵);
- 空间复杂度:O(m+n) (最坏情况所有行 / 列都有 0);
- 实现代码:
function setZeroes(matrix: number[][]): void { const rows = matrix.length; const cols = matrix[0].length; // 用集合存储需要置零的行和列索引 const zeroRows = new Set<number>(); const zeroCols = new Set<number>(); // 第一步:遍历矩阵,记录所有 0 的行和列 for (let i = 0; i < rows; i++) { for (let j = 0; j < cols; j++) { if (matrix[i][j] === 0) { zeroRows.add(i); zeroCols.add(j); } } } // 第二步:根据记录的行和列置零 for (let i = 0; i < rows; i++) { for (let j = 0; j < cols; j++) { if (zeroRows.has(i) || zeroCols.has(j)) { matrix[i][j] = 0; } } } } - 思路:
-
实现方式二:原地优化 (利用矩阵首行 / 首列存储标记)
- 思路:
- 利用矩阵首行和首列代替 【解法 1】 中的集合,存储需要置零的行 / 列标记;
- 先单独记录首行 / 首列本身是否有 0 (避免覆盖原始 0 的信息);
- 遍历矩阵 (从第二行第二列开始),若当前元素为 0,则将首行对应列、首列对应行置为 0;
- 根据首行 / 首列的标记,置零对应行 / 列;
- 最后根据初始记录,决定是否置零首行 / 首列;
- 复杂度:
- 时间复杂度:仍为 O(mn) (多次遍历但无嵌套层级增加);
- 空间复杂度:降至 O(1) (仅用两个布尔变量),符合 “原地修改” 的最优要求;
- 实现代码:
function setZeroes(matrix: number[][]): void { const rows = matrix.length; const cols = matrix[0].length; // 标记首行/首列是否本身有0 let firstRowHasZero = false; let firstColHasZero = false; // 第一步:检查首行是否有0 for (let j = 0; j < cols; j++) { if (matrix[0][j] === 0) { firstRowHasZero = true; break; } } // 第二步:检查首列是否有0 for (let i = 0; i < rows; i++) { if (matrix[i][0] === 0) { firstColHasZero = true; break; } } // 第三步:遍历矩阵(排除首行首列),用首行首列标记需要置零的行/列 for (let i = 1; i < rows; i++) { for (let j = 1; j < cols; j++) { if (matrix[i][j] === 0) { matrix[i][0] = 0; // 标记第i行需要置零 matrix[0][j] = 0; // 标记第j列需要置零 } } } // 第四步:根据首列标记,置零对应行 for (let i = 1; i < rows; i++) { if (matrix[i][0] === 0) { for (let j = 1; j < cols; j++) { matrix[i][j] = 0; } } } // 第五步:根据首行标记,置零对应列 for (let j = 1; j < cols; j++) { if (matrix[0][j] === 0) { for (let i = 1; i < rows; i++) { matrix[i][j] = 0; } } } // 第六步:处理首行 if (firstRowHasZero) { for (let j = 0; j < cols; j++) { matrix[0][j] = 0; } } // 第七步:处理首列 if (firstColHasZero) { for (let i = 0; i < rows; i++) { matrix[i][0] = 0; } } } - 思路:
合并两个有序数组
-
思路:双指针 (从后到前,最优解),利用 nums1 末尾的预留空间 (0 的位置),从两个数组的末尾有效元素开始比较,将较大的元素直接放入 nums1 的末尾,无需额外空间;
- 初始化指针:l 指向 nums1 有效元素末尾 (m-1),r2 指向 nums2 末尾 (n-1),r 指向 nums1 最终末尾 (m+n-1);
- 比较 nums1[l] 和 nums2[r2],将较大的放入 nums1[r],并移动对应指针;
- 若 nums2 还有剩余元素 (r >= 0),将剩余元素全部放入 nums1 (nums1 剩余位置无需处理,因为本身就是有效元素);
-
复杂度:
- 时间复杂度:O(m+n);
- 空间复杂度:O(1) (无额外空间);
-
实现代码:
function merge3(nums1: number[], m: number, nums2: number[], n: number): void { // 初始化指针:left→nums1 有效末尾,right2→nums2 末尾,right→nums1 最终末尾 let left = m - 1, right2 = n - 1, right = m + n - 1; // 步骤1:从后向前比较,填充较大元素 while (left >= 0 && right2 >= 0) { if (nums1[left] >= nums2[right2]) { nums1[right--] = nums1[left--]; } else { nums1[right--] = nums2[right2--]; } } // 步骤2:处理 nums2 剩余元素(nums1剩余无需处理,本身就是有序的) while (right2 >= 0) { nums1[right--] = nums2[right2--]; } }
矩阵对角线元素的和
-
实现方式一:基础遍历
- 思路:
- 获取矩阵边长 n (正方形矩阵行 / 列数相等);
- 遍历每一行 i,分别取主对角线元素 mat[i][i] 和副对角线元素 mat[i][n-1-i] (主对角线:行号 = 列号,副对角线:行号 + 列号 = n-1);
- 若
i === n-1-i(奇数阶矩阵中心元素),则只加一次,否则两个元素都加; - 累加所有符合条件的元素得到结果;
- 复杂度:
- 时间复杂度:O(n) (仅遍历 n 行);
- 空间复杂度:O(1) (仅用常量变量);
- 实现代码:
function diagonalSum(mat: number[][]): number { const n = mat.length; // 矩阵边长 let sum = 0; for (let i = 0; i < n; i++) { const main = mat[i][i]; // 主对角线元素 const anti = mat[i][n - 1 - i]; // 副对角线元素 if (i === n - 1 - i) { sum += main; // 中心元素只加一次 } else { sum += main + anti; } } return sum; } - 思路:
-
实现方式二:无判断累加 (简化逻辑)
- 思路:
- 先累加所有主对角线 + 副对角线元素;
- 若矩阵阶数为奇数,减去重复计算的中心元素 (因为中心元素被加了两次);
- 复杂度:
- 时间复杂度:O(n) (仅遍历 n 行);
- 空间复杂度:O(1) (仅用常量变量);
- 实现代码:
function diagonalSum(mat: number[][]): number { const n = mat.length; let sum = 0; // 累加主、副对角线所有元素(可能重复) for (let i = 0; i < n; i++) { sum += mat[i][i]; // 主对角线 sum += mat[i][n - 1 - i]; // 副对角线 } // 奇数阶矩阵:减去重复的中心元素 if (n % 2 === 1) { const mid = Math.floor(n / 2); sum -= mat[mid][mid]; } return sum; } - 思路:
-
实现方式三:累加对角线元素后将其置 0 (避免重复计算 / 标记已处理)
- 思路:
- 遍历主对角线,累加元素值后将该位置置 0;
- 遍历副对角线,累加元素值 (此时中心元素已被置 0,无需额外判断);
- 最终累加和即为结果 (中心元素仅在主对角线遍历中被加一次);
- 复杂度:
- 时间复杂度:O(n) (仅遍历 n 行);
- 空间复杂度:O(1) (仅用常量变量);
- 实现代码:
function diagonalSum(mat: number[][]): number { const n = mat.length; let sum = 0; // 主对角线累加并置0 for (let i = 0; i < n; i++) { sum += mat[i][i]; mat[i][i] = 0; sum += mat[i][n - 1 - i]; } return sum; } - 思路:
-
实现方式四:双指针 (优化遍历,仅遍历对角线元素)
- 思路:
- 用两个指针 left (主对角线列号) 和 right (副对角线列号);
- 遍历每一行,left 从 0 递增到 n-1,right 从 n-1 递减到 0;
- 累加当前行的 mat[i][left] 和 mat[i][right],若
left === right则只加一次;
- 复杂度:
- 时间复杂度:O(n) (仅遍历 n 行);
- 空间复杂度:O(1) (仅用常量变量);
- 实现代码:
function diagonalSum(mat: number[][]): number { const n = mat.length; let sum = 0; let left = 0; let right = n - 1; for (let i = 0; i < n; i++) { if (left === right) { sum += mat[i][left]; } else { sum += mat[i][left] + mat[i][right]; } left++; right--; } return sum; } - 思路:
整数反转
-
方式一:数学取余法 (高效,符合题目 “不存储 64 位整数” 要求)
- 思路:
- 通过取余 (%10) 提取数字的最后一位,通过整除 (Math.trunc(x / 10)) 去掉最后一位;
- 逐位构建反转后的数字 (result = result * 10 + 余数);
- 关键:在每一步构建时检查是否超出范围 (避免溢出),而不是最后检查 (符合 “不存储 64 位整数” 的限制);
- 正数:若
result > MAX/10,则result*10必然超过 MAX;若result === MAX/10,则最后一位不能超过 7 (MAX=2147483647); - 负数:若
result < MIN/10,则result*10必然低于 MIN;若result === MIN/10,则最后一位不能低于 -8 (MIN=-2147483648);
- 正数:若
- 复杂度:
- 时间复杂度:O(n),循环次数 = 数字位数;
- 空间复杂度:O(1);
- 实现代码:
function reverse(x: number): number { const MAX = 2 ** 31 - 1; // 2147483647 const MIN = - (2 ** 31); // -2147483648 let result = 0; while (x !== 0) { // 提取最后一位数字(处理负数:-123 % 10 = -3,符合预期) const digit = x % 10; // 去掉最后一位(Math.trunc 处理负数更准确:Math.trunc(-123/10) = -12) x = Math.trunc(x / 10); // 提前检查溢出:在乘以10加余数前判断是否超出范围 // 正数溢出:result > MAX/10 或 (result === MAX/10 且 digit > 7)(MAX最后一位是7) if (result > Math.trunc(MAX / 10) || (result === Math.trunc(MAX / 10) && digit > 7)) return 0; // 负数溢出:result < Math.trunc(MIN/10) 或 (result === Math.trunc(MIN/10) 且 digit < -8) if (result < Math.trunc(MIN / 10) || (result === Math.trunc(MIN / 10) && digit < -8)) return 0; // 构建反转数字 result = result * 10 + digit; } return result; } - 思路:
-
方式二:递归法 (拓展思路,不推荐生产使用)
- 思路:
- 通过递归逐位提取最后一位,构建反转数字,同时在递归过程中检查溢出;
- 每一步递归提取最后一位,更新反转结果,并检查溢出;
- 递归深度取决于数字位数 (最多 10 位,无栈溢出风险),但性能略低于迭代的数学法,仅作思路拓展;
- 复杂度:
- 时间复杂度:O(n),递归次数 = 数字位数;
- 空间复杂度:O(1);
- 实现代码:
function reverse(x: number): number { const MAX = 2 ** 31 - 1; const MIN = - (2 ** 31); // 递归辅助函数:参数为剩余数字、当前反转结果 const helper = (remaining: number, res: number): number => { if (remaining === 0) return res; const digit = remaining % 10; const newRemaining = Math.trunc(remaining / 10); // 溢出检查(同数学法) if (res > Math.trunc(MAX / 10) || (res === Math.trunc(MAX / 10) && digit > 7)) return 0; if (res < Math.trunc(MIN / 10) || (res === Math.trunc(MIN / 10) && digit < -8)) return 0; return helper(newRemaining, res * 10 + digit); }; return helper(x, 0); } - 思路:
杨辉三角
-
方式一:基础迭代法
- 思路:
- 初始化结果数组,逐行构建;
- 每行先初始化全 1 的数组 (满足首尾为 1 的特性);
- 遍历每行的中间元素,中间的每个元素等于上一行中相邻两个元素之和 (即 row[i] = prevRow[i-1] + prevRow[i]);
- 复杂度:
- 时间复杂度:O(n²) (n 为行数);
- 空间复杂度:O(n²) (n 为行数);
- 实现代码:
function generate(numRows: number): number[][] { // 边界处理:行数为 0 时返回空数组 if (numRows === 0) return []; // 初始化结果数组,第一行固定为 [1] const result: number[][] = [[1]]; // 从第 2 行开始构建(i 代表当前行号,从 1 开始,对应第 2 行) for (let i = 1; i < numRows; i++) { // 初始化当前行为全 1 的数组,长度为 i+1(第 i+1 行有 i+1 个元素) const currentRow: number[] = new Array(i + 1).fill(1); // 获取上一行数据 const prevRow = result[i - 1]; // 计算中间元素(首尾已经是 1,无需处理) // 遍历范围:从第 1 个元素到倒数第 2 个元素 for (let j = 1; j < currentRow.length - 1; j++) { currentRow[j] = prevRow[j - 1] + prevRow[j]; } // 将当前行加入结果 result.push(currentRow); } return result; }; - 思路:
-
方式二:递归法 (分治思路)
- 思路:
- 递归核心:生成第 n 行的前提是先生成第 n-1 行;
- 终止条件:
- 当
n = 1时,返回 [[1]]; - 递归获取前 n-1 行的结果,再基于最后一行计算第 n 行;
- 当
- 复杂度:
- 时间复杂度:总操作次数:
1 + Σ(i 从 1 到 n-1)i = 1 + n(n-1)/2,最终时间复杂度为 O(n²); - 空间复杂度:O(n²);
- 时间复杂度:总操作次数:
- 实现代码:
function generate(numRows: number): number[][] { // 终止条件 1:行数为 0 if (numRows === 0) return []; // 终止条件 2:行数为 1 if (numRows === 1) return [[1]]; // 递归获取前 numRows - 1 行的结果 const prevResult = generate(numRows - 1); // 获取上一行数据 const prevRow = prevResult[prevResult.length - 1]; // 构建当前行 const currentRow: number[] = [1]; // 计算中间元素 for (let j = 1; j < prevRow.length; j++) { currentRow.push(prevRow[j - 1] + prevRow[j]); } currentRow.push(1); // 将当前行加入结果并返回 return [...prevResult, currentRow]; }; - 思路:
字母异位词分组
-
方式一:排序键值法
- 思路:
- 对每个字符串排序,异位词排序后结果相同 (如 eat 和 tea 排序后都是 aet);
- 用排序后的字符串作为 Map 的键,值为对应异位词数组,遍历完成后返回 Map 的值即可;
- 复杂度:
- 时间复杂度:O(n∗klogk),n 是字符串数组长度,k 是单个字符串的最大长度,每个字符串排序的时间是 O(klogk),遍历 n 个字符串总时间为 O(n∗klogk);
- 空间复杂度:O(n∗k),Map 存储所有字符串,总空间为所有字符串的字符数之和;
- 实现代码:
function groupAnagrams(strs: string[]): string[][] { // 定义Map,键为排序后的字符串,值为异位词数组 const map = new Map<string, string[]>(); for (const str of strs) { // 1. 拆分字符串为字符数组 → 排序 → 重新拼接成键 const key = str.split('').sort().join(''); // 2. 若键不存在则初始化空数组,存在则直接追加当前字符串 if (!map.has(key)) { map.set(key, []); } map.get(key)!.push(str); } // 3. 将Map的值转换为数组返回 return Array.from(map.values()); } - 思路:
-
方式二:计数键值法(优化排序耗时)
- 思路:
- 异位词的字符计数完全相同 (如 eat 和 tea 都是 a:1, e:1, t:1);
- 用字符计数的字符串 (如 a1e1t1) 作为 Map 键,避免排序的 O(klogk) 耗时;
- 复杂度:
- 时间复杂度:O(n∗k),遍历每个字符串的每个字符 (O(n∗k)),计数和生成键的操作是 O(26) (常数),无排序耗时;
- 空间复杂度:O(n∗k),Map 存储所有字符串,计数数组是常数空间 (26 长度),不影响整体复杂度;
- 实现代码:
function groupAnagramsByCount(strs: string[]): string[][] { const map = new Map<string, string[]>(); for (const str of strs) { // 1. 初始化26个字母的计数数组(对应a-z) const count = new Array(26).fill(0); // 2. 统计每个字符出现次数 for (const char of str) { const index = char.charCodeAt(0) - 'a'.charCodeAt(0); count[index]++; } // 3. 生成计数键(如a1e1t1) const key = count.map((num, i) => num > 0 ? `${String.fromCharCode(97 + i)}${num}` : '').join(''); // 4. 存入Map if (!map.has(key)) { map.set(key, []); } map.get(key)!.push(str); } return Array.from(map.values()); } - 思路:
螺旋矩阵
-
思路:
-
复杂度:
- 时间复杂度:
- 空间复杂度:
-
实现代码:
螺旋矩阵 II
{% lc https://leetcode.cn/problems/spiral-matrix-ii/ 59.螺旋矩阵II %}-
思路:
-
复杂度:
- 时间复杂度:
- 空间复杂度:
-
实现代码:
算法基础🔮 时间复杂度、空间复杂度
上一篇