数组、二维数组与基础算法思维 | JavaSE
数组、二维数组与基础算法思维
一、学习目标
完成本章后,你应该能够:
- 能够解释数组(Array)解决什么问题以及数组的基本特点。
- 能够使用静态初始化和动态初始化创建数组。
- 能够使用索引访问和修改数组元素。
- 能够正确使用
length并识别数组越界问题。 - 能够解释动态初始化后各种类型数组元素的默认值。
- 能够使用循环完成数组遍历。
- 能够独立实现数组求和、平均值、最大值、最小值、查找、交换、逆序和统计。
- 能够理解数组变量保存的是数组对象的引用,并分析两个变量指向同一数组时的行为。
- 能够创建、访问和遍历二维数组。
- 能够理解 Java 二维数组本质上是“数组的数组”,因此可以形成不规则二维数组。
- 能够开始建立遍历、枚举、状态记录、最值更新、双指针和随机交换等基础算法思维。
二、核心知识
2.1 为什么需要数组
变量一次通常保存一个值:
double score1 = 90.5;
double score2 = 88.0;
double score3 = 96.5;
double score4 = 77.0;
如果一个班有:
8 名学生
50 名学生
500 名学生
显然不应该创建:
score1
score2
score3
...
score500
数组就是解决这一问题的基本数据结构。
数组是一个用于存储一批相同类型数据的数据容器。
例如:
double[] scores = {
90.5,
88.0,
96.5,
77.0
};
现在所有成绩被组织在一个数组中。
2.2 数组的核心特点
基础阶段需要首先建立:
一个数组
↓
存储一批元素
↓
元素具有统一类型
↓
每个元素具有索引
↓
数组创建后长度固定
例如:
int[] numbers = {10, 20, 30};
可以理解:
索引 0 1 2
┌────┬────┬────┐
元素 │ 10 │ 20 │ 30 │
└────┴────┴────┘
2.3 数组是一种引用类型
Java 数据类型前面已经区分:
基本数据类型
引用数据类型
数组属于:
引用类型。
例如:
int[] arr = {10, 20, 30};
变量:
arr
不是三个整数本身。
它保存的是:
对这个数组对象的引用。
可以先建立简单模型:
arr
│
│ 引用
↓
┌────┬────┬────┐
│ 10 │ 20 │ 30 │
└────┴────┴────┘
这个思想将在下一组面向对象中变得非常重要。
2.4 数组静态初始化
如果创建数组时已经知道元素:
int[] numbers = {10, 20, 30};
这是最常见的静态初始化简化格式。
完整格式:
int[] numbers = new int[]{10, 20, 30};
通用格式:
数据类型[] 数组名 = {元素1, 元素2, ...};
或者:
数据类型[] 数组名 =
new 数据类型[]{元素1, 元素2, ...};
2.5 int[] arr 与 int arr[]
Java 都允许:
int[] arr;
和:
int arr[];
但是现代 Java 代码一般更推荐:
int[] arr;
因为:
int[]
更加清晰地表达:
这是一个 int 数组类型。
2.6 动态初始化
如果一开始只知道:
需要存多少个数据。
但还不知道具体内容,可以:
int[] arr = new int[5];
这里:
5
表示数组长度。
数组创建后:
索引:
0 1 2 3 4
一共:
5 个元素
2.7 数组长度一旦创建就固定
例如:
int[] arr = new int[5];
这个数组对象的长度就是:
5
不能把同一个数组对象“扩容”成:
10
如果需要另一个长度:
arr = new int[10];
实际上是:
创建了一个新的长度为 10 的数组,并让
arr改为引用这个新数组。
不是原来的数组自己变长。
2.8 数组索引
访问数组元素:
数组名[索引]
例如:
int[] arr = {10, 20, 30};
System.out.println(arr[0]);
System.out.println(arr[1]);
System.out.println(arr[2]);
得到:
10
20
30
Java 数组:
从索引 0 开始。
长度为 n 的数组合法索引范围:
0 ~ n - 1
2.9 为什么最后一个索引是 length - 1
长度:
3
数组:
索引:
0
1
2
所以:
最后索引 = 3 - 1 = 2
一般化:
长度 n
↓
合法索引:
0 ~ n-1
因此后面遍历通常写:
i < arr.length
而不是:
i <= arr.length
2.10 获取数组长度
数组提供:
arr.length
例如:
int[] arr = {10, 20, 30};
System.out.println(arr.length);
得到:
3
注意:
arr.length
不是:
arr.length()
数组使用的是:
length
字段。
2.11 修改数组元素
数组元素可以重新赋值:
int[] arr = {10, 20, 30};
arr[1] = 100;
System.out.println(arr[1]);
结果:
100
数组变成:
10 100 30
所以数组是一个:
可以修改元素内容的数据容器。
2.12 数组越界
假设:
int[] arr = {10, 20, 30};
合法:
arr[0]
arr[1]
arr[2]
非法:
arr[3]
因为:
3 >= arr.length
Java 会在运行时检查数组访问。
非法访问会产生:
ArrayIndexOutOfBoundsException
因此:
for (int i = 0; i <= arr.length; i++)
是一个典型错误。
正确通常是:
for (int i = 0; i < arr.length; i++)
2.13 动态初始化数组的默认值
例如:
int[] arr = new int[3];
虽然没有手动赋值,但数组元素创建时具有默认值。
常见规则:
| 元素类型 | 默认值 |
| --------------------------- | ---------- |
| byte / short / int / long | 0 |
| float / double | 0.0 |
| char | '\u0000' |
| boolean | false |
| 引用类型 | null |
例如:
int[] numbers = new int[3];
System.out.println(numbers[0]);
得到:
0
2.14 数组默认值与局部变量不是一回事
数组组件:
int[] arr = new int[3];
自动得到默认值。
但是普通局部变量:
int number;
不能在未初始化时直接:
System.out.println(number);
所以不要形成:
Java 中所有变量都会自动初始化。
更准确地说:
数组元素有默认初始化
而普通未初始化局部变量需要满足 Java 的明确赋值规则后才能读取。
2.15 什么是数组遍历
遍历(Traversal):
将数组中的元素按照某种顺序一个一个访问。
例如:
int[] arr = {20, 30, 40, 50};
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
可以观察:
i = 0 → arr[0]
i = 1 → arr[1]
i = 2 → arr[2]
i = 3 → arr[3]
循环变量:
i
天然可以作为数组索引。
这就是上一章循环与数组产生连接的地方。
2.16 数组求和
例如:
int[] arr = {10, 20, 30, 40};
int sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
System.out.println(sum);
算法模型:
sum = 0
↓
遍历每一个元素
↓
把元素累计到 sum
2.17 数组平均值
先求总和:
double[] scores = {90, 80, 70, 100};
double sum = 0;
for (int i = 0; i < scores.length; i++) {
sum += scores[i];
}
再:
double average = sum / scores.length;
这里体现:
遍历
+
累加
+
统一处理结果
2.18 数组最大值
假设:
int[] arr = {100, 50, 90, 60, 80};
不应该写:
int max = 0;
因为如果数组全部是负数:
-10 -20 -30
那么 0 根本不是数组中的数据。
更稳妥的初始化:
int max = arr[0];
然后从后续元素开始比较:
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
核心算法:
假设第一个是最大值
↓
逐个拿后面的数据比较
↓
发现更大的
↓
更新 max
2.19 最值算法的本质:维护当前最优解
循环过程中:
max
始终表示:
截止目前已经看过的数据中的最大值。
例如:
数据:
50 90 60 100
过程:
max = 50
看到 90
→ max = 90
看到 60
→ max 仍然 90
看到 100
→ max = 100
这是非常经典的:
状态维护思想。
2.20 数组查找
需求:
找出指定元素第一次出现的索引。
例如:
int[] arr = {10, 20, 30, 20};
int target = 20;
int index = -1;
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
index = i;
break;
}
}
System.out.println(index);
这里:
-1
通常表示:
尚未找到 / 不存在。
发现目标:
记录索引
+
break
这就是最基础的:
线性查找(Linear Search)思想。
2.21 数组元素交换
交换:
arr[i]
arr[j]
不能直接:
arr[i] = arr[j];
arr[j] = arr[i];
因为第一句执行后,原来的 arr[i] 已经丢失。
需要临时变量:
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
模型:
A → temp
B → A
temp → B
这是算法中非常基础的操作。
2.22 数组逆序
例如:
1 2 3 4 5
变成:
5 4 3 2 1
可以使用:
左指针
右指针
向中间移动:
1 2 3 4 5
↑ ↑
i j
交换:
arr[i]
arr[j]
然后:
i++
j--
直到:
i >= j
这就是双指针思想的第一次接触。
2.23 两个数组变量指向同一个数组
观察:
int[] a = {10, 20, 30};
int[] b = a;
b[0] = 100;
System.out.println(a[0]);
这里:
int[] b = a;
并没有复制一个完整新数组。
而是把:
a 中的数组引用
赋给:
b
所以:
a ──┐
├──→ [10, 20, 30]
b ──┘
执行:
b[0] = 100;
修改的其实是:
两个变量共同指向的那个数组对象。
所以通过:
a[0]
看到的也会是:
100
2.24 数组变量与数组对象要分开理解
例如:
int[] arr = new int[3];
可以区分:
arr
↓
数组变量
new int[3]
↓
数组对象
变量:
arr
保存引用。
真正的元素:
0
0
0
存在数组对象中。
这会直接为后面的:
类
对象
引用
null
做准备。
2.25 什么是二维数组
一维数组:
int[] row = {10, 20, 30};
二维数组:
int[][] matrix = {
{10, 20, 30},
{40, 50, 60},
{70, 80, 90}
};
可以先看成表格:
10 20 30
40 50 60
70 80 90
但 Java 更本质的理解是:
二维数组是“数组中的元素仍然是数组”。
即:
int[][]
↓
数组
↓
每个元素的类型是 int[]
2.26 二维数组静态初始化
例如:
int[][] arr = {
{10, 20, 30},
{40, 50},
{60, 70, 80, 90}
};
注意三行长度:
3
2
4
完全不同。
这说明:
Java 二维数组并不要求每一行长度相同。
所以所谓“二维数组”不一定是数学意义上的规则矩阵。
2.27 二维数组的引用结构
上面的二维数组可以理解为:
arr
│
↓
┌──────┬──────┬──────┐
│ ref │ ref │ ref │
└─┬────┴─┬────┴─┬────┘
│ │ │
↓ ↓ ↓
[10,20,30]
[40,50]
[60,70,80,90]
因此:
arr[0]
得到的是:
第一行那个
int[]数组。
而:
arr[0][1]
得到:
第一行数组中的索引 1 元素。
即:
20
2.28 二维数组动态初始化
例如:
int[][] arr = new int[3][5];
可以理解为:
创建 3 行
每一行创建长度为 5 的 int[]
形成:
3 × 5
结构。
2.29 arr.length 与 arr[i].length
二维数组:
int[][] arr = {
{1, 2, 3},
{4, 5},
{6, 7, 8, 9}
};
arr.length
表示:
外层数组元素个数
也可以理解:
行数
这里是:
3
而:
arr[0].length
是:
3
arr[1].length
是:
2
所以遍历二维数组时不能机械认为每一行长度完全相同。
2.30 二维数组遍历
标准写法:
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + "\t");
}
System.out.println();
}
外层:
遍历每一行数组
内层:
遍历当前这一行的全部元素
所以:
arr[i].length
而不是统一写死:
arr[0].length
更加通用。
三、使用方法
3.1 学生成绩录入
import java.util.Scanner;
public class ScoreDemo {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
double[] scores = new double[8];
for (int i = 0; i < scores.length; i++) {
System.out.print(
"请输入第 " + (i + 1) + " 名学生成绩:"
);
scores[i] = scanner.nextDouble();
}
}
}
数组解决:
存储 8 个成绩
循环解决:
重复录入 8 次
3.2 计算平均分、最高分和最低分
double sum = 0;
double max = scores[0];
double min = scores[0];
for (int i = 0; i < scores.length; i++) {
double score = scores[i];
sum += score;
if (score > max) {
max = score;
}
if (score < min) {
min = score;
}
}
double average = sum / scores.length;
一次遍历同时完成:
求和
求最大
求最小
3.3 查找元素
public static int findIndex(
int[] arr,
int target
) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
return i;
}
}
return -1;
}
这已经把:
数组
循环
if
方法
return
真正组合了起来。
3.4 数组原地逆序
public static void reverse(int[] arr) {
int left = 0;
int right = arr.length - 1;
while (left < right) {
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
left++;
right--;
}
}
这里没有创建:
第二个同长度数组
而是在原数组上交换。
这种方式叫:
原地(In-place)处理。
3.5 随机交换实现基础洗牌
假设:
int[] arr = {1, 2, 3, 4, 5};
可以选择随机位置并交换。
例如基础练习:
import java.util.Random;
Random random = new Random();
for (int i = 0; i < arr.length; i++) {
int randomIndex =
random.nextInt(arr.length);
int temp = arr[i];
arr[i] = arr[randomIndex];
arr[randomIndex] = temp;
}
这可以让元素顺序发生随机变化。
作为基础数组练习足够。
如果未来需要研究严格的均匀洗牌算法,应进一步学习标准的 Fisher–Yates Shuffle;当前章节先掌握随机交换与数组操作。
3.6 斗地主做牌思路
源课程中的简化斗地主案例要求:
制作 54 张牌
+
洗牌
可以设计:
String[] cards = new String[54];
然后:
循环生成牌面
↓
依次放入数组
↓
Random 随机交换
↓
完成洗牌
这个案例的重要价值不是斗地主本身,而是综合:
数组存储
循环生成
索引访问
元素交换
随机数
3.7 二维数组统计班级成绩
例如:
double[][] scores = {
{90, 80, 70},
{88, 92},
{60, 70, 80, 90}
};
计算每个班级平均分:
for (int i = 0; i < scores.length; i++) {
double sum = 0;
for (int j = 0; j < scores[i].length; j++) {
sum += scores[i][j];
}
double average =
sum / scores[i].length;
System.out.println(
"第 " + (i + 1)
+ " 个班平均分:"
+ average
);
}
二维数组可以自然表达:
多个班级
↓
每个班级多个学生
四、原理与进阶
4.1 Java 数组是对象
从 Java 语言模型看:
数组是对象,并且数组对象在运行时创建。
因此:
int[] arr = new int[5];
不是简单“声明 5 个变量”。
而是:
创建数组对象
↓
产生数组引用
↓
arr 保存这个引用
这也是:
int[] b = arr;
不会自动深复制数组的原因。
4.2 数组长度为什么不能修改
数组对象创建时:
new int[5]
长度已经确定。
它的:
length
表示组件数量。
如果数据数量以后需要:
不断增长
不断缩小
固定长度数组就不够方便。
后面学习:
ArrayList
集合框架
正是为动态数据管理提供更方便的抽象。
4.3 二维数组不是连续数学矩阵模型
Java:
int[][]
本质是:
数组的元素类型仍然是 int[]
因此允许:
int[][] arr = {
{1},
{2, 3},
{4, 5, 6}
};
甚至可以先创建外层:
int[][] arr = new int[3][];
再分别创建:
arr[0] = new int[2];
arr[1] = new int[5];
arr[2] = new int[1];
这再次说明:
Java 多维数组本质上是嵌套数组,而不是强制规则矩阵。
4.4 数组算法的通用模式
很多数组题看起来不同,其实可以归纳为几个基本模板。
模式一:遍历
一个一个访问
模式二:累加
sum += current
模式三:计数
满足条件
→ count++
模式四:最值维护
发现更优
→ 更新 best
模式五:查找
满足目标
→ 记录位置
→ 必要时提前结束
模式六:交换
temp
模式七:双指针
左端 + 右端
→ 向中间移动
模式八:二维枚举
外层行
内层列
真正掌握这些模式后,大量基础数组题都会变成:
已知模板的组合。
4.5 从“写代码”升级到“分析算法”
例如:
找数组最大值。
不要第一反应直接敲代码。
应该先说:
输入:
一个数组
目标:
找到最大元素
状态:
当前最大值 max
初始状态:
max = 第一个元素
处理:
遍历剩余元素
更新规则:
current > max
→ max = current
输出:
max
这已经是在编写:
算法设计。
Java 代码只是把这个算法翻译成具体语法。
4.6 空数组是一个重要边界
Java 可以创建:
int[] arr = new int[0];
它的:
length = 0
这是合法数组。
但是:
arr[0]
不合法。
同样:
int max = arr[0];
这种最大值算法如果允许传入空数组,就会产生问题。
这说明算法设计必须考虑:
正常数据
+
边界数据
+
非法输入
这种意识会贯穿整个软件开发过程。
五、实践应用
数组是计算机科学最基础的数据结构之一。
典型应用:
成绩列表
传感器数据
游戏地图
图片像素
矩阵
查找算法
排序算法
缓冲区
固定大小数据集合
二维数组常见:
棋盘
座位表
地图
成绩表
矩阵
游戏关卡
图像像素网格
同时,数组也是以后学习:
ArrayList
HashMap
栈
队列
树
图
排序
搜索
动态规划
时非常重要的基础。
六、常见问题
6.1 数组索引为什么从 0 开始?
Java 数组规定:
长度为 n
↓
合法索引 0 ~ n-1
基础阶段首先按语言规则正确使用。
从数据结构和地址偏移角度理解 0-based indexing 可以在后续学习计算机组成和数据结构时进一步深入。
6.2 arr.length 为什么没有括号?
因为数组的:
length
是数组提供的长度字段,而不是一个普通方法调用。
所以:
arr.length
而不是:
arr.length()
6.3 为什么 i <= arr.length 会出错?
数组最后一个合法索引:
arr.length - 1
当:
i == arr.length
时已经越界。
因此通常写:
i < arr.length
6.4 数组长度可以是 0 吗?
可以。
new int[0]
是合法空数组。
但其中没有任何可访问元素。
6.5 动态数组为什么里面已经有数据?
例如:
new int[5]
数组组件创建时会按照类型获得默认值。
因此不是“什么都不存在”,而是:
0 0 0 0 0
6.6 数组变量本身可以是 null 吗?
可以:
int[] arr = null;
这表示:
arr 当前没有引用任何数组对象
如果此时:
arr.length
就会出现空引用相关问题。
null 和对象引用将在面向对象阶段继续深入。
6.7 int[] b = a 是复制数组吗?
不是。
它复制的是:
数组引用的值。
因此两个变量可以引用同一个数组对象。
6.8 二维数组每一行必须长度相同吗?
不必须。
例如:
int[][] arr = {
{1, 2},
{3},
{4, 5, 6}
};
完全合法。
6.9 为什么二维数组遍历使用 arr[i].length?
因为:
arr[i]
本身就是当前行数组。
每一行长度可能不同。
所以应该获取:
当前行自己的 length
6.10 最大值为什么不能总初始化成 0?
因为数组可能是:
-100 -50 -20
如果:
int max = 0;
最终可能错误得到:
0
但 0 根本不在数组中。
更稳妥的方式通常是:
int max = arr[0];
前提是数组非空。
6.11 数组和 ArrayList 是不是一样?
不是。
数组:
固定长度
Java 语言内建结构
ArrayList:
集合框架中的类
更方便进行动态数量管理
后面会专门学习。
七、练习与验收
7.1 知识问答
- 什么是数组?
- 为什么大量同类型数据适合使用数组?
- 数组属于基本类型还是引用类型?
- 数组静态初始化有哪些常见格式?
- 什么是动态初始化?
- 数组创建后长度能否修改?
- 数组索引从多少开始?
- 长度为
n的数组最大合法索引是多少? arr.length表示什么?- 数组越界会发生什么?
- 动态初始化后的元素默认值分别是什么?
- 什么叫遍历?
- 为什么数组遍历经常使用
i < arr.length? - 数组求最大值的基本思想是什么?
- 为什么最大值初始值不应该机械写成
0? int[] b = a后,a和b是两个数组对象还是两个引用变量?- 什么是二维数组?
- 为什么二维数组可以理解为“数组中的元素仍然是数组”?
arr[i]与arr[i][j]分别是什么?- 二维数组每行长度必须相同吗?
7.2 代码阅读
禁止运行:
public class ArrayRead01 {
public static void main(String[] args) {
int[] a = {10, 20, 30};
int[] b = a;
b[0] = 100;
System.out.println(a[0]);
System.out.println(b[0]);
}
}
要求:
- 预测两行输出。
a和b是两个数组对象吗?- 画出引用关系图。
- 如果希望得到真正独立的新数组,需要解决什么问题?
继续分析:
public class ArrayRead02 {
public static void main(String[] args) {
int[][] arr = {
{1, 2},
{3, 4, 5},
{6}
};
System.out.println(arr.length);
System.out.println(arr[1].length);
System.out.println(arr[1][2]);
}
}
要求:
- 预测三行输出。
- 分别解释三个表达式访问了什么。
- 画出二维数组结构。
7.3 手写代码
- 创建长度为 5 的整数数组并遍历。
- 计算数组全部元素之和。
- 计算平均值。
- 寻找最大值。
- 寻找最小值。
- 查找指定元素第一次出现的索引,不存在返回
-1。 - 统计某元素出现次数。
- 交换两个指定位置元素。
- 原地逆序数组。
- 录入 8 名学生 Java 成绩并输出平均、最高、最低分。
- 创建
3 × 5二维数组并遍历。 - 计算二维数组每一行的和。
- 找到二维数组最大元素及其行列索引。
7.4 Debug
public class ArrayDebug01 {
public static void main(String[] args) {
int[] arr = {10, 20, 30};
for (int i = 0; i <= arr.length; i++) {
System.out.println(arr[i]);
}
}
}
要求:
- 找出错误。
- 判断是编译期错误还是运行期错误。
- 解释合法索引范围。
- 修复程序。
继续:
int[] arr = {-10, -20, -5};
int max = 0;
for (int value : arr) {
if (value > max) {
max = value;
}
}
System.out.println(max);
要求:
- 判断算法结果是否正确。
- 找出问题不在语法,而在哪里。
- 设计更合理的最大值初始化方案。
7.5 综合训练
综合案例一:班级 Java 成绩
录入 8 名学生成绩。
最终输出:
全部成绩
总分
平均分
最高分
最低分
要求:
- 必须使用数组保存数据。
- 不允许定义 8 个独立成绩变量。
- 合理拆分方法。
综合案例二:随机洗牌
准备一个数组:
1 ~ 54
模拟 54 张牌。
要求:
创建数组
↓
初始化 54 个元素
↓
使用 Random
↓
随机交换数组元素
↓
输出打乱后的结果
完成后思考:
当前随机交换方法是否保证所有排列具有完全相同的概率?
把这个问题保留到后续算法学习阶段。
综合案例三:班级成绩二维模型
使用二维数组表示:
三个班级
每班人数可以不同
要求:
- 输出全部成绩。
- 计算每个班平均分。
- 找出全体学生最高分。
- 记录最高分属于第几个班、第几个学生。
7.6 本章验收
关闭资料后确认:
- [ ] 能从零创建静态数组。
- [ ] 能从零创建动态数组。
- [ ] 能正确访问和修改元素。
- [ ] 能解释
length与最大合法索引。 - [ ] 能识别数组越界。
- [ ] 能说出数组默认值。
- [ ] 能完成数组遍历。
- [ ] 能完成求和、平均值、最大值和最小值。
- [ ] 能完成简单线性查找。
- [ ] 能交换数组元素。
- [ ] 能原地逆序数组。
- [ ] 能解释数组引用。
- [ ] 能从零创建二维数组。
- [ ] 能遍历不规则二维数组。
- [ ] 能解释
arr[i]与arr[i][j]。 - [ ] 能把数组问题先描述成算法步骤,再翻译成 Java。
如果目前只能:
“照着模板写
for (int i = 0; i < arr.length; i++)”
却不能解释:
i 为什么从 0 开始
为什么必须 < length
arr[i] 到底是什么
那么数组仍然只是会写,还没有真正掌握。