JAVA多重数组理解
文章目录
- 概念
- 多重数组概念
- 声明与初始化
- 1. 声明
- 2. 初始化方式
- 方式一:直接字面量初始化(推荐用于小数据)
- 方式二:指定大小(规则矩形)
- 方式三:锯齿数组(Jagged Array)— 每行长度不同
- 访问与修改元素
- 遍历多重数组
- 1. 普通 for 循环(知道行列数)
- 2. 增强 for 循环(for-each,简洁安全)
- 内存结构(重要理解)
- 常见使用场景(面试高频)
- 实用工具方法
- 1. 获取行列数
- 2. 深拷贝二维数组(避免引用共享)
- 常见陷阱 & 注意事项
- 数组跟链表
- 扩展:高维数组(了解即可)
概念
在 Java 中,“多重数组”通常指的是多维数组(Multidimensional Arrays),最常见的是二维数组(如矩阵),但也可以有三维、四维等。
多重数组概念
Java 的多重数组本质上是 “数组的数组”(array of arrays),即:
- 一维数组:
int[] arr - 二维数组:
int[][] matrix→ 每个元素是一个int[] - 三维数组:
int[][][] cube→ 每个元素是一个int[][]
Java 的多维数组不要求每行长度相同(称为“锯齿数组”或“不规则数组”)。
声明与初始化
1. 声明
int[][] matrix; // 声明一个二维整型数组
String[][][] data; // 三维字符串数组
2. 初始化方式
方式一:直接字面量初始化(推荐用于小数据)
int[][] grid = {{1, 2, 3},{4, 5, 6},{7, 8, 9}
};
方式二:指定大小(规则矩形)
int rows = 3, cols = 4;
int[][] mat = new int[rows][cols]; // 3x4 的全 0 矩阵
方式三:锯齿数组(Jagged Array)— 每行长度不同
int[][] jagged = new int[3][]; // 3 行,列数未定
jagged[0] = new int[2]; // 第0行有2列
jagged[1] = new int[5]; // 第1行有5列
jagged[2] = new int[1]; // 第2行有1列
注意:
new int[3][4]是规则数组;new int[3][]是锯齿数组(需手动初始化每行)。
访问与修改元素
int value = matrix[i][j]; // 读取第 i 行第 j 列
matrix[i][j] = 100; // 修改
边界检查:Java 会在运行时自动检查下标是否越界(抛出 ArrayIndexOutOfBoundsException)。
遍历多重数组
1. 普通 for 循环(知道行列数)
for (int i = 0; i < matrix.length; i++) {for (int j = 0; j < matrix[i].length; j++) {System.out.print(matrix[i][j] + " ");}System.out.println();
}
🔍
matrix.length→ 行数
matrix[i].length→ 第 i 行的列数(支持锯齿数组!)
2. 增强 for 循环(for-each,简洁安全)
for (int[] row : matrix) {for (int val : row) {System.out.print(val + " ");}System.out.println();
}
推荐:优先使用 for-each,避免下标错误,尤其处理锯齿数组时更安全。
内存结构(重要理解)
Java 的二维数组不是一块连续内存(不像 C/C++),而是:
- 一个一维数组,每个元素指向另一个一维数组
- 所以可以每行长度不同(锯齿)
matrix → [ ref0, ref1, ref2 ]↓ ↓ ↓[1,2] [3,4,5] [6]
优势:灵活;劣势:缓存局部性较差(相比连续内存)
常见使用场景(面试高频)
| 场景 | 示例 |
|---|---|
| 矩阵操作 | 旋转矩阵、螺旋遍历、对角线遍历 |
| 动态规划 | 二维 DP 表(如 LCS、编辑距离) |
| 图的邻接矩阵 | graph[i][j] == 1 表示 i→j 有边 |
| 棋盘/地图模拟 | 迷宫、岛屿数量、生命游戏 |
| 分组存储 | 每行代表一类数据 |
实用工具方法
1. 获取行列数
int rows = matrix.length;
int cols = matrix[0].length; // 注意:仅当至少有一行且非空时安全!
✅ 安全写法(处理空数组):
if (matrix == null || matrix.length == 0) return;
int rows = matrix.length;
int cols = matrix[0].length; // 此时可安全访问
2. 深拷贝二维数组(避免引用共享)
int[][] deep = new int[original.length][];
for (int i = 0; i < original.length; i++) {deep[i] = original[i].clone(); // 克隆每一行
}
| 类型 | 含义 | 特点 |
|---|---|---|
| 浅拷贝(Shallow Copy) | 复制对象的引用,不复制内部对象 | 新旧数组共享子对象 |
| 深拷贝(Deep Copy) | 递归复制所有层级的对象 | 完全独立,互不影响 |
| 数组类型 | 浅拷贝是否安全? | 如何实现深拷贝 |
|---|---|---|
int[], double[] 等基本类型 | ✅ 安全(值拷贝) | arr.clone() 即可 |
String[] | ⚠️ 表面安全(String 不可变) | 通常 clone() 足够 |
int[][](二维基本类型) | ❌ 不安全 | 循环 + row.clone() |
Object[](含可变对象) | ❌ 不安全 | 需手动深拷贝每个元素(可能递归) |
🔔 注意:
String虽是引用类型,但不可变(immutable),所以浅拷贝通常不会出问题。但如果是StringBuilder[],就必须深拷贝!
| 误区 | 正确理解 |
|---|---|
“clone() 就是深拷贝” | ❌ 默认是浅拷贝,除非重写 clone() 方法 |
“Arrays.copyOf() 是深拷贝” | ❌ 对多维数组仍是浅拷贝 |
| “基本类型数组不需要深拷贝” | ✅ 正确!因为存的是值,不是引用 |
常见陷阱 & 注意事项
| 问题 | 说明 |
|---|---|
| 空指针异常 | matrix 为 null,或某行为 null(锯齿数组未初始化) |
| 列数不一致 | 误以为所有行长度相同,直接用 matrix[0].length 遍历所有行 |
| 浅拷贝问题 | 直接赋值导致修改副本影响原数组 |
| 内存浪费 | 用二维数组存稀疏矩阵(此时应考虑 Map<Pair, Value> 或稀疏表示) |
数组跟链表
数组(Array)和链表(Linked List)是两种最基础、最重要的线性数据结构。它们在内存布局、操作效率、适用场景上有本质区别。掌握它们的差异,是算法设计和系统优化的关键。
| 特性 | 数组(Array) | 链表(Linked List) |
|---|---|---|
| 内存布局 | 连续内存块 | 非连续,靠指针连接 |
| 随机访问 | ✅ O(1)(通过下标) | ❌ O(n)(必须遍历) |
| 插入/删除(中间) | ❌ O(n)(需移动元素) | ✅ O(1)(已知节点时) |
| 插入/删除(头部) | ❌ O(n)(除非用特殊技巧) | ✅ O(1) |
| 插入/删除(尾部) | ✅ O(1)(动态数组均摊) | ✅ O(1)(若有 tail 指针) |
| 空间开销 | 仅数据本身 | 每个节点额外存指针(如 next) |
| 缓存友好性 | ✅ 高(局部性好) | ❌ 低(内存跳跃) |
| 大小固定? | 静态数组固定;动态数组可扩容 | 动态伸缩,无需预分配 |
| 场景 | 推荐结构 | 原因 |
|---|---|---|
| 需要频繁随机访问(如排序、DP) | ✅ 数组 | O(1) 访问 |
| 频繁在头部/中间插入删除 | ✅ 链表 | O(1) 修改指针 |
| 元素数量固定或可预估 | ✅ 数组 | 内存紧凑,性能高 |
| 实现栈(只操作尾部) | ✅ 数组 or 链表 | 两者都 O(1) |
| 实现队列(首尾操作) | ✅ 链表 or 循环数组 | 链表天然支持;数组需循环缓冲 |
| 内存敏感(小对象大量存储) | ✅ 数组 | 链表指针开销大 |
| 需要缓存友好(高性能计算) | ✅ 数组 | 连续内存,预取高效 |
扩展:高维数组(了解即可)
// 三维数组:3 层,每层 4 行,每行 5 列
int[][][] cube = new int[3][4][5];// 访问
cube[0][1][2] = 10;// 遍历
for (int[][] layer : cube) {for (int[] row : layer) {for (int val : row) {// ...}}
}
📌 实际开发中,三维以上数组较少见,通常用对象封装更清晰。
