【Java】P7 Java数组完全指南:从基础到进阶
目录
- 1 数组基本概念
- 1.1 什么是数组
- 1.2 数组的本质特点
- 1.3 数组的概念
- 2 一维数组操作
- 2.1 声明与初始化
- 方式一:静态初始化
- 方式二:动态初始化
- 类型推断(Java 10+):让编译器自动判断类型
- 2.2 核心操作详解
- 访问和修改元素
- 获取数组长度
- 数组遍历的三种方式
- 实际应用示例:计算平均分
- 2.3 默认初始化值
- 3. 内存模型深度解析
- 3.1 JVM内存区域比喻
- 3.2 数组创建过程详解
- 3.3 引用赋值与浅拷贝陷阱
- 3.4 垃圾回收机制
- 4. 二维数组进阶
- 4.1 二维数组的概念理解
- 4.2 声明与初始化详解
- 静态初始化:直接指定所有元素
- 动态初始化:先声明后赋值
- 4.3 访问与遍历详解
- 访问特定元素
- 获取维度信息
- 完整遍历二维数组
- 4.4 二维数组默认初始化
- 5. 常用算法实现
- 5.1 数组统计算法详解
- 基础统计:求和、平均值、最值
- 进阶统计:中位数
- 5.2 数组复制深度解析
- 硬拷贝和软拷贝
- 各种复制方法的性能对比
- 5.3 数组反转算法
- 方法1:双指针法(推荐,原地操作)
- 方法2:创建新数组(不修改原数组)
- 5.4 数组扩容与缩容详解
- 6. 搜索与排序算法深度解析
- 6.1 搜索算法对比
- 线性搜索(顺序搜索)
- 二分搜索(折半搜索)
- 搜索效率对比演示
- 6.2 排序算法详解
- 冒泡排序:最容易理解的排序
- 选择排序:每次选出最小值
- 插入排序:像整理手中的牌
- 快速排序:分而治之的典型代表
- 排序算法性能对比
- 7. Arrays工具类深度应用
- 7.1 常用方法详解
- 数组比较方法
- 数组转字符串方法
- 数组填充方法
- 数组排序方法
- 数组搜索方法
- 7.2 高级应用技巧
- 数组拷贝的多种方式对比
- 数组类型转换技巧
1 数组基本概念
1.1 什么是数组
想象一下,你有一排连续的储物柜,每个柜子里都放着同一类型的物品(比如都是书)。数组就是这样的概念——它是存储相同数据类型元素的有序集合,就像一排编了号的储物柜。
为什么需要数组?如果我们要存储30本书的名称,难道要创建30个变量吗?
// 不使用数组:非常麻烦
int book1 = 85, book2 = 92, book3 = 78, ...
// 如果要计算平均分,需要写很长的代码// 使用数组:简洁高效
int[] books = {85, 92, 78, ...};
1.2 数组的本质特点
- 有序性: 元素按照放入的顺序排列,第一个是索引0,第二个是索引1
- 同质性: 所有元素必须是相同的数据类型
- 引用类型: 数组变量存储的是内存地址,不是实际数据
- 长度固定: 一旦创建,长度不能改变(这是与List的重要区别)
1.3 数组的概念
数组是存储相同数据类型元素的有序集合,通过索引访问元素。本质上,数组是一个引用数据类型,在内存中占用连续空间。
2 一维数组操作
2.1 声明与初始化
数组的创建分为两个步骤:
- 声明(告诉编译器要创建什么类型的数组)和
- 初始化(实际创建数组并分配内存)
方式一:静态初始化
在创建数组的同时就指定元素值,系统会自动计算数组长度。
// 标准写法
int[] scores = new int[]{95, 87, 92, 78, 85};// 简化写法(更常用)
int[] scores = {95, 87, 92, 78, 85};// 其他类型的示例
String[] names = {"张三", "李四", "王五", "赵六"};
double[] prices = {19.9, 25.5, 99.0, 149.9};
boolean[] flags = {true, false, true, true};
方式二:动态初始化
先指定数组长度,后面再给元素赋值。这种方式适合你知道需要多少空间,但暂时不知道具体值的情况。
// 1. 先创建指定长度的数组
int[] numbers = new int[5]; // 创建能存储5个整数的数组// 2. 再逐个赋值
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;// 此时 numbers 数组等价于 {10, 20, 30, 0, 0}
为什么会有两种方式?
- 静态初始化: 适合你已经知道要存储什么数据的情况
- 动态初始化: 适合你知道需要多大空间,但数据需要运行时计算的情况
// 静态:存储已知的学生姓名
String[] classmates = {"小明", "小红", "小刚"};// 动态:存储用户输入的成绩(运行时才知道具体值)
Scanner scanner = new Scanner(System.in);
int[] inputScores = new int[3];
for (int i = 0; i < 3; i++) {System.out.print("请输入第" + (i+1) + "个成绩:");inputScores[i] = scanner.nextInt();
}
类型推断(Java 10+):让编译器自动判断类型
// 传统写法
int[] numbers = new int[]{1, 2, 3, 4, 5};// 类型推断写法
var numbers = new int[]{1, 2, 3, 4, 5}; // var会被推断为int[]
2.2 核心操作详解
访问和修改元素
数组访问就像是告诉管理员:“请给我第X号储物柜里的东西"或"请把这个东西放到第X号储物柜”。
int[] scores = {85, 92, 78, 95, 88};// 获取元素(读取操作)
int firstScore = scores[0]; // 获取第一个成绩:85
int thirdScore = scores[2]; // 获取第三个成绩:78// 修改元素(写入操作)
scores[2] = 100; // 把第三个成绩改为100
System.out.println(scores[2]); // 输出:100// 注意:索引必须在有效范围内
// scores[5] = 90; // 错误!会抛出ArrayIndexOutOfBoundsException
获取数组长度
length
是数组对象的一个属性(不是方法,所以不需要括号)
int[] numbers = {10, 20, 30, 40, 50};
int size = numbers.length; // 获取长度:5// 常用于循环中避免硬编码
for (int i = 0; i < numbers.length; i++) {System.out.println("索引" + i + "的值是:" + numbers[i]);
}
数组遍历的三种方式
- 传统for循环: 当你需要索引时使用
int[] scores = {95, 87, 92, 78, 85};for (int i = 0; i < scores.length; i++) {System.out.println("学生" + (i+1) + "的成绩:" + scores[i]);// 输出:学生1的成绩:95// 学生2的成绩:87// ...
}
- 增强for循环(for-each): 当你只需要元素值时使用(推荐)
int[] scores = {95, 87, 92, 78, 85};for (int score : scores) { // 读作:对于scores中的每一个scoreSystem.out.println("成绩:" + score);
}
// 注意:for-each循环中不能修改数组元素
- while循环: 特殊情况使用
int[] scores = {95, 87, 92, 78, 85};
int i = 0;
while (i < scores.length) {System.out.println("成绩:" + scores[i]);i++;
}
实际应用示例:计算平均分
public static double calculateAverage(int[] scores) {if (scores.length == 0) return 0; // 避免除零错误int sum = 0;for (int score : scores) {sum += score;}return (double) sum / scores.length; // 强制类型转换确保得到小数
}// 使用示例
int[] classScores = {95, 87, 92, 78, 85};
double average = calculateAverage(classScores);
System.out.println("班级平均分:" + average); // 输出:87.4
2.3 默认初始化值
当你使用动态初始化创建数组时,系统会自动给每个元素分配默认值。这就像是给每个空储物柜贴上一个默认的标签。
为什么有默认值?
Java要求所有变量在使用前必须有值,所以系统自动给了一个"安全"的默认值。
数据类型 | 默认值 | 原因解释 | 示例代码 |
---|---|---|---|
byte、short、int、long | 0 | 整数的"零值"最安全 | int[] nums = new int[3]; → {0, 0, 0} |
float、double | 0.0 | 浮点数的"零值" | double[] prices = new double[2]; → {0.0, 0.0} |
char | ‘\u0000’ | Unicode的空字符 | char[] chars = new char[2]; → {'\u0000', '\u0000'} |
boolean | false | 布尔型的"假"值 | boolean[] flags = new boolean[2]; → {false, false} |
引用类型 | null | 表示"没有指向任何对象" | String[] names = new String[2]; → {null, null} |
实际示例对比:
// 动态初始化 - 有默认值
int[] numbers = new int[5];
System.out.println(Arrays.toString(numbers));
// 输出:[0, 0, 0, 0, 0]String[] names = new String[3];
System.out.println(Arrays.toString(names));
// 输出:[null, null, null]// 静态初始化 - 没有默认值概念,直接是你指定的值
int[] scores = {95, 87, 92};
System.out.println(Arrays.toString(scores));
// 输出:[95, 87, 92]
注意事项:
String[] names = new String[3];
// 此时names[0]是null,不是空字符串""!
if (names[0] == null) {System.out.println("第一个元素是null"); // 会执行这行
}// 如果要使用,需要先赋值
names[0] = "张三";
System.out.println(names[0].length()); // 现在可以安全使用了
3. 内存模型深度解析
理解内存模型是掌握数组的关键。很多初学者在这里容易困惑,我们用生活化的比喻来解释。
3.1 JVM内存区域比喻
想象内存是一座大楼:
虚拟机栈(Stack) - 就像办公楼里的办公桌
- 存储方法内的局部变量(包括数组引用变量)
- 空间小但访问速度快
- 方法执行完毕,桌面就清空了
堆内存(Heap) - 就像大楼里的仓库
- 存储所有对象的实际数据(包括数组元素)
- 空间大但访问相对较慢
- 需要垃圾回收器(GC)来清理无用数据
3.2 数组创建过程详解
让我们跟踪一个数组从创建到使用的完整过程:
public static void main(String[] args) {// 步骤1:在栈中声明引用变量int[] arr1; // 栈中有了arr1这个"便利贴",但还没写地址// 步骤2:在堆中创建数组对象,并把地址赋给变量值arr1 = new int[4]; // 堆中分配空间存储{0,0,0,0},栈中arr1记录地址// 步骤3:通过引用访问和修改堆中的数据arr1[0] = 10; // 通过arr1找到堆中的数组,修改第1个元素arr1[2] = 20; // 修改第3个元素System.out.println(Arrays.toString(arr1)); // 输出:[10, 0, 20, 0]
}
内存示意图:
栈内存(Stack) 堆内存(Heap)
┌──────────────┐ ┌──---───────────────┐
│ arr1: 0x001 │ ────→ │ 0x001: [10,0,20,0] │
└──────────────┘ └──---───────────────┘
3.3 引用赋值与浅拷贝陷阱
最容易犯错的一个场景,容易将赋值地址误以为是赋值数组值。
public static void main(String[] args) {// 创建第一个数组int[] arr1 = new int[]{5, 6, 7};// 这里发生了什么?int[] arr2 = arr1; // ← 关键:这不是复制数组,而是复制地址!// 修改arr2,看看arr1会不会变化arr2[1] = 99;System.out.println("arr1[1] = " + arr1[1]); // 输出:99(惊不惊喜?)System.out.println("arr2[1] = " + arr2[1]); // 输出:99
}
为什么会这样?
执行 int[] arr2 = arr1; 后的内存状态:栈内存 堆内存
┌──────────────┐ ┌─────────────────┐
│ arr1: 0x001 │ ────→ │ │
│ arr2: 0x001 │ ────→ │ 0x001: [5,99,7] │
└──────────────┘ └─────────────────┘
两个引用变量指向同一个堆内存区域,所以修改其中一个,另一个也会"看到"变化。
如何真正复制数组?
int[] arr1 = {5, 6, 7};// 方法1:使用clone()方法
int[] arr2 = arr1.clone();// 方法2:使用Arrays.copyOf()
int[] arr3 = Arrays.copyOf(arr1, arr1.length);// 方法3:手动复制
int[] arr4 = new int[arr1.length];
for (int i = 0; i < arr1.length; i++) {arr4[i] = arr1[i];
}// 现在修改arr2不会影响arr1
arr2[1] = 99;
System.out.println("arr1[1] = " + arr1[1]); // 输出:6
System.out.println("arr2[1] = " + arr2[1]); // 输出:99
3.4 垃圾回收机制
什么时候会发生垃圾回收?
public static void main(String[] args) {int[] arr = new int[]{1, 2, 3, 4, 5};System.out.println("arr指向:" + Arrays.toString(arr));// 重新分配:原来的数组变成垃圾arr = new int[]{10, 20, 30}; System.out.println("arr现在指向:" + Arrays.toString(arr));// 原来的{1,2,3,4,5}数组没有任何引用指向它,成为垃圾// GC会在合适的时机自动回收这块内存
}
手动提示垃圾回收:
int[] bigArray = new int[1000000]; // 创建大数组
// 使用完毕后
bigArray = null; // 断开引用,帮助GC回收
System.gc(); // 建议JVM进行垃圾回收(不保证立即执行)
4. 二维数组进阶
4.1 二维数组的概念理解
二维数组就像是一个表格或矩阵。如果一维数组是一排储物柜,那么二维数组就是多排储物柜组成的储物间。
生活化比喻:
一维数组:一排座位 [座位1, 座位2, 座位3]
二维数组:电影院多排座位
第1排:[座位1, 座位2, 座位3]
第2排:[座位1, 座位2, 座位3]
第3排:[座位1, 座位2, 座位3]
4.2 声明与初始化详解
静态初始化:直接指定所有元素
// 表示一个3行3列的成绩表
int[][] classScores = {{85, 92, 78}, // 第1排学生的成绩{95, 88, 91}, // 第2排学生的成绩 {87, 93, 89} // 第3排学生的成绩
};// 也可以写成一行(但可读性较差)
int[][] scores = {{85, 92, 78}, {95, 88, 91}, {87, 93, 89}};
动态初始化:先声明后赋值
二维数组的动态初始化如一维数组的动态初始化,先声明数组整体结构,然后赋值。
// 创建3行4列的数组
int[][] matrix = new int[3][4];// 给每个位置赋值
matrix[0][0] = 1;
matrix[1][1] = 6;
matrix[2][2] = 11;// 结果:
// [1, 0, 0, 0]
// [0, 6, 0, 0]
// [0, 0, 11, 0]
4.3 访问与遍历详解
访问特定元素
int[][] scores = {{85, 92, 78}, {95, 88, 91}, {87, 93, 89}};// 访问第2行第3列的元素(注意索引从0开始)
int element = scores[1][2]; // 获取91
System.out.println("第2排第3个学生的成绩:" + element);// 修改元素
scores[0][1] = 100; // 把第1排第2个学生的成绩改为100
获取维度信息
int[][] matrix = {{1, 2, 3, 4}, {5, 6, 7, 8}};int rows = matrix.length; // 行数:2
int colsInFirstRow = matrix[0].length; // 第1行的列数:4
int colsInSecondRow = matrix[1].length; // 第2行的列数:4System.out.println("这是一个" + rows + "行" + colsInFirstRow + "列的矩阵");
完整遍历二维数组
- 嵌套传统for循环(需要索引时使用)
int[][] scores = {{85, 92, 78}, {95, 88, 91}, {87, 93, 89}};for (int row = 0; row < scores.length; row++) {for (int col = 0; col < scores[row].length; col++) {System.out.print("第" + (row+1) + "排第" + (col+1) + "个学生成绩:" + scores[row][col] + " ");}System.out.println(); // 换行
}
- 嵌套增强for循环(推荐,代码最简洁)
int[][] scores = {{85, 92, 78}, {95, 88, 91}, {87, 93, 89}};for (int[] row : scores) { // 对于每一行for (int score : row) { // 对于这一行中的每个成绩System.out.print(score + " ");}System.out.println();
}
4.4 二维数组默认初始化
// 创建3行2列的整型数组
int[][] matrix = new int[3][2];
System.out.println(Arrays.deepToString(matrix));
// 输出:[[0, 0], [0, 0], [0, 0]]// 创建2行3列的字符串数组
String[][] names = new String[2][3];
System.out.println(Arrays.deepToString(names));
// 输出:[[null, null, null], [null, null, null]]// 只指定行数,不指定列数
String[][] irregular = new String[3][];
System.out.println(Arrays.toString(irregular));
// 输出:[null, null, null] ← 外层元素都是null// 为某一行分配空间
irregular[1] = new String[2];
System.out.println(Arrays.deepToString(irregular));
// 输出:[null, [null, null], null]// 如果试图访问没有分配空间的行的元素,会出错
try {irregular[0][0] = "测试"; // NullPointerException!
} catch (NullPointerException e) {System.out.println("错误:第1行还没有分配空间");
}
5. 常用算法实现
5.1 数组统计算法详解
统计操作是处理数组时最常见的需求。让我们从简单到复杂逐步实现。
基础统计:求和、平均值、最值
public class ArrayStatistics {/*** 计算数组的基本统计信息* @param arr 输入数组*/public static void calculateBasicStats(int[] arr) {if (arr == null || arr.length == 0) {System.out.println("数组为空,无法计算");return;}// 初始化变量long sum = 0; // 使用long防止大数溢出int max = arr[0]; // 假设第一个元素是最大值int min = arr[0]; // 假设第一个元素是最小值// 遍历数组进行统计for (int num : arr) {sum += num; // 累加求和max = Math.max(max, num); // 更新最大值min = Math.min(min, num); // 更新最小值}double average = (double) sum / arr.length; // 计算平均值// 输出结果System.out.println("数组长度:" + arr.length);System.out.println("总和:" + sum);System.out.printf("平均值:%.2f%n", average);System.out.println("最大值:" + max);System.out.println("最小值:" + min);System.out.println("极差:" + (max - min));}// 使用示例public static void main(String[] args) {int[] testScores = {85, 92, 78, 95, 88, 91, 87, 93, 89, 94};calculateBasicStats(testScores);// 输出:// 数组长度:10// 总和:892// 平均值:89.20// 最大值:95// 最小值:78// 极差:17}
}
进阶统计:中位数
public static void calculateAdvancedStats(int[] arr) {if (arr == null || arr.length == 0) return;// 创建副本避免修改原数组int[] sortedArr = arr.clone();Arrays.sort(sortedArr);// 计算中位数double median;int n = sortedArr.length;if (n % 2 == 0) {// 偶数个元素:取中间两个数的平均值median = (sortedArr[n/2 - 1] + sortedArr[n/2]) / 2.0;} else {// 奇数个元素:取中间的数median = sortedArr[n/2];}
}
5.2 数组复制深度解析
数组复制是一个容易出错的概念,让我们详细分析各种复制方式的区别。
硬拷贝和软拷贝
想象你有一本书的目录页:
- 浅拷贝: 复制目录页,但书的内容还是同一本书
- 深拷贝: 不仅复制目录页,还把整本书的内容都重新抄写一遍
public class ArrayCopyDemo {/*** 演示引用赋值(不是真正的复制)*/public static void demonstrateReferenceAssignment() {System.out.println("=== 引用赋值演示 ===");int[] original = {1, 2, 3, 4, 5};int[] reference = original; // 这不是复制!只是创建了另一个引用System.out.println("修改前:");System.out.println("original: " + Arrays.toString(original));System.out.println("reference: " + Arrays.toString(reference));// 通过reference修改数组reference[0] = 999;System.out.println("通过reference修改后:");System.out.println("original: " + Arrays.toString(original)); // [999, 2, 3, 4, 5]System.out.println("reference: " + Arrays.toString(reference)); // [999, 2, 3, 4, 5]System.out.println("两个数组是同一个对象:" + (original == reference)); // true}/*** 演示真正的数组复制*/public static void demonstrateArrayCopy() {System.out.println("\n=== 数组复制演示 ===");int[] original = {1, 2, 3, 4, 5};// 方法1:使用clone()int[] copy1 = original.clone();// 方法2:使用Arrays.copyOf()int[] copy2 = Arrays.copyOf(original, original.length);// 方法3:使用System.arraycopy()int[] copy3 = new int[original.length];System.arraycopy(original, 0, copy3, 0, original.length);// 方法4:手动循环复制int[] copy4 = new int[original.length];for (int i = 0; i < original.length; i++) {copy4[i] = original[i];}// 修改原数组original[0] = 999;System.out.println("修改original后:");System.out.println("original: " + Arrays.toString(original)); // [999, 2, 3, 4, 5]System.out.println("copy1: " + Arrays.toString(copy1)); // [1, 2, 3, 4, 5]System.out.println("copy2: " + Arrays.toString(copy2)); // [1, 2, 3, 4, 5]System.out.println("copy3: " + Arrays.toString(copy3)); // [1, 2, 3, 4, 5]System.out.println("copy4: " + Arrays.toString(copy4)); // [1, 2, 3, 4, 5]}
}
各种复制方法的性能对比
public static void performanceBenchmark() {int[] largeArray = new int[1000000];Arrays.fill(largeArray, 42); // 填充测试数据// 测试clone()方法long start = System.nanoTime();int[] copy1 = largeArray.clone();long cloneTime = System.nanoTime() - start;// 测试Arrays.copyOf()方法start = System.nanoTime();int[] copy2 = Arrays.copyOf(largeArray, largeArray.length);long copyOfTime = System.nanoTime() - start;// 测试System.arraycopy()方法start = System.nanoTime();int[] copy3 = new int[largeArray.length];System.arraycopy(largeArray, 0, copy3, 0, largeArray.length);long arraycopyTime = System.nanoTime() - start;// 测试手动循环start = System.nanoTime();int[] copy4 = new int[largeArray.length];for (int i = 0; i < largeArray.length; i++) {copy4[i] = largeArray[i];}long loopTime = System.nanoTime() - start;System.out.println("复制100万个整数的性能对比:");System.out.printf("clone(): %d ns%n", cloneTime);System.out.printf("Arrays.copyOf(): %d ns%n", copyOfTime);System.out.printf("System.arraycopy():%d ns%n", arraycopyTime);System.out.printf("手动循环: %d ns%n", loopTime);
}
5.3 数组反转算法
数组反转是一个经典的算法题,有多种实现方法。
方法1:双指针法(推荐,原地操作)
public static void reverseArrayInPlace(int[] arr) {if (arr == null || arr.length <= 1) return;int left = 0; // 左指针从开头开始int right = arr.length - 1; // 右指针从末尾开始while (left < right) {// 交换左右指针指向的元素int temp = arr[left];arr[left] = arr[right];arr[right] = temp;// 移动指针left++;right--;}
}// 使用示例
int[] numbers = {1, 2, 3, 4, 5, 6};
System.out.println("反转前:" + Arrays.toString(numbers)); // [1, 2, 3, 4, 5, 6]
reverseArrayInPlace(numbers);
System.out.println("反转后:" + Arrays.toString(numbers)); // [6, 5, 4, 3, 2, 1]
方法2:创建新数组(不修改原数组)
public static int[] reverseArrayCopy(int[] arr) {if (arr == null) return null;int[] reversed = new int[arr.length];for (int i = 0; i < arr.length; i++) {reversed[i] = arr[arr.length - 1 - i]; // 从末尾开始取元素}return reversed;
}// 使用示例
int[] original = {1, 2, 3, 4, 5};
int[] reversed = reverseArrayCopy(original);
System.out.println("原数组:" + Arrays.toString(original)); // [1, 2, 3, 4, 5]
System.out.println("反转后:" + Arrays.toString(reversed)); // [5, 4, 3, 2, 1]
5.4 数组扩容与缩容详解
数组长度固定,但我们可以通过创建新数组来模拟扩容和缩容。
int[] arr = new int[]{1,2,3,4,5};
int[] newarr = new int[arr.length * 2];
// 不如
int[] newarr = new int[arr.length << 1];
arr = newarr
GC 会根据实际情况,比如在通过 arr = newarr
命令后更换指针,从而 GC 自动删除 arr
地址下的值,从而回收内存,减少内存空间浪费。
6. 搜索与排序算法深度解析
6.1 搜索算法对比
线性搜索(顺序搜索)
这就像在一排书中一本一本地找你要的书。
public class SearchAlgorithms {/*** 线性搜索:从头到尾逐个检查* 时间复杂度:O(n)* 空间复杂度:O(1)* 优点:简单,适用于任何数组(无需排序)* 缺点:效率低,大数组查找慢*/public static int linearSearch(int[] arr, int target) {if (arr == null) return -1;for (int i = 0; i < arr.length; i++) {if (arr[i] == target) {System.out.println("线性搜索:检查了" + (i + 1) + "个元素");return i; // 返回找到的索引}}return -1; // 没有找到}
}
二分搜索(折半搜索)
这就像在字典中查单词,每次翻到中间,根据字母顺序决定往前还是往后找。
/*** 二分搜索:在有序数组中快速查找* 时间复杂度:O(logn)* 空间复杂度:O(1)* 前提:数组必须已排序* 优点:效率高,大数组查找很快* 缺点:需要排序,插入删除后需重新排序*/
public static int binarySearch(int[] sortedArray, int target) {if (sortedArray == null || sortedArray.length == 0) return -1;int left = 0;int right = sortedArray.length - 1;int steps = 0; // 记录查找步数while (left <= right) {steps++;// 防止大数溢出的写法int mid = left + (right - left) / 2;System.out.println("第" + steps + "步:检查索引" + mid + ",值为" + sortedArray[mid]);if (sortedArray[mid] == target) {System.out.println("二分搜索:用了" + steps + "步找到");return mid;} else if (sortedArray[mid] < target) {left = mid + 1; // 目标在右半部分} else {right = mid - 1; // 目标在左半部分}}System.out.println("二分搜索:用了" + steps + "步,未找到");return -1;
}
搜索效率对比演示
public static void compareSearchMethods() {// 创建大数组进行测试int[] largeArray = new int[100000];for (int i = 0; i < largeArray.length; i++) {largeArray[i] = i * 2; // 偶数数组:0, 2, 4, 6, ...}int target = 99998; // 查找倒数第二个元素// 线性搜索long startTime = System.nanoTime();int linearResult = linearSearch(largeArray, target);long linearTime = System.nanoTime() - startTime;// 二分搜索startTime = System.nanoTime();int binaryResult = binarySearch(largeArray, target);long binaryTime = System.nanoTime() - startTime;System.out.println("在10万个元素中查找" + target + ":");System.out.println("线性搜索结果:索引" + linearResult + ",用时:" + linearTime + "ns");System.out.println("二分搜索结果:索引" + binaryResult + ",用时:" + binaryTime + "ns");System.out.println("二分搜索比线性搜索快:" + (linearTime / binaryTime) + "倍");
}
6.2 排序算法详解
冒泡排序:最容易理解的排序
就像水中的气泡一样,小的元素会慢慢"冒"到前面。
public class SortingAlgorithms {/*** 冒泡排序:相邻元素两两比较,大的往后"冒"* 时间复杂度:O(n²) - 最坏和平均情况* 时间复杂度:O(n) - 最好情况(已排序)* 空间复杂度:O(1)* 稳定性:稳定(相等元素顺序不变)*/public static void bubbleSort(int[] arr) {if (arr == null || arr.length <= 1) return;int n = arr.length;boolean hasSwapped; // 优化:检测是否已排序for (int i = 0; i < n - 1; i++) {hasSwapped = false;// 每轮比较,最大元素会"冒泡"到末尾for (int j = 0; j < n - i - 1; j++) {if (arr[j] > arr[j + 1]) {// 交换相邻元素swap(arr, j, j + 1);hasSwapped = true;}}// 如果这一轮没有交换,说明已经排序完成if (!hasSwapped) {System.out.println("冒泡排序在第" + (i + 1) + "轮后完成");break;}System.out.println("第" + (i + 1) + "轮后:" + Arrays.toString(arr));}}/*** 辅助方法:交换数组中两个元素*/private static void swap(int[] arr, int i, int j) {int temp = arr[i];arr[i] = arr[j];arr[j] = temp;}
}
选择排序:每次选出最小值
/*** 选择排序:每次从未排序部分选择最小元素,放到已排序部分末尾* 时间复杂度:O(n²) - 所有情况都是* 空间复杂度:O(1)* 稳定性:不稳定*/
public static void selectionSort(int[] arr) {if (arr == null || arr.length <= 1) return;int n = arr.length;for (int i = 0; i < n - 1; i++) {// 假设当前位置就是最小值int minIndex = i;// 在剩余未排序部分找真正的最小值for (int j = i + 1; j < n; j++) {if (arr[j] < arr[minIndex]) {minIndex = j;}}// 如果找到更小的值,就交换if (minIndex != i) {swap(arr, i, minIndex);}System.out.println("第" + (i + 1) + "轮选择排序后:" + Arrays.toString(arr));}
}
插入排序:像整理手中的牌
/*** 插入排序:将每个元素插入到已排序部分的正确位置* 时间复杂度:O(n²) - 最坏情况,O(n) - 最好情况* 空间复杂度:O(1)* 稳定性:稳定* 适用:小数组或基本有序的数组*/
public static void insertionSort(int[] arr) {if (arr == null || arr.length <= 1) return;for (int i = 1; i < arr.length; i++) {int current = arr[i]; // 待插入的元素int j = i - 1; // 已排序部分的末尾// 向后移动比current大的元素while (j >= 0 && arr[j] > current) {arr[j + 1] = arr[j];j--;}// 插入current到正确位置arr[j + 1] = current;System.out.println("插入" + current + "后:" + Arrays.toString(arr));}
}
快速排序:分而治之的典型代表
/*** 快速排序:选择一个基准,将数组分为小于和大于基准的两部分,递归排序* 时间复杂度:O(n log n) - 平均情况,O(n²) - 最坏情况* 空间复杂度:O(log n) - 递归栈* 稳定性:不稳定* 优点:实际应用中很快,原地排序*/
public static void quickSort(int[] arr) {if (arr == null || arr.length <= 1) return;quickSortHelper(arr, 0, arr.length - 1);
}private static void quickSortHelper(int[] arr, int low, int high) {if (low < high) {// 分割数组,返回基准位置int pivotIndex = partition(arr, low, high);System.out.println("基准" + arr[pivotIndex] + "就位,当前状态:" + Arrays.toString(Arrays.copyOfRange(arr, low, high + 1)));// 递归排序基准左右两部分quickSortHelper(arr, low, pivotIndex - 1);quickSortHelper(arr, pivotIndex + 1, high);}
}private static int partition(int[] arr, int low, int high) {// 选择最后一个元素作为基准int pivot = arr[high];int i = low - 1; // 小于基准的元素的边界for (int j = low; j < high; j++) {// 如果当前元素小于等于基准if (arr[j] <= pivot) {i++;swap(arr, i, j);}}// 将基准放到正确位置swap(arr, i + 1, high);return i + 1;
}
排序算法性能对比
public static void sortingBenchmark() {int[] sizes = {1000, 5000, 10000};for (int size : sizes) {System.out.println("\n=== 数组大小:" + size + " ===");// 生成随机测试数据int[] original = generateRandomArray(size);// 测试冒泡排序int[] bubbleArray = original.clone();long start = System.currentTimeMillis();bubbleSort(bubbleArray);long bubbleTime = System.currentTimeMillis() - start;// 测试选择排序int[] selectionArray = original.clone();start = System.currentTimeMillis();selectionSort(selectionArray);long selectionTime = System.currentTimeMillis() - start;// 测试插入排序int[] insertionArray = original.clone();start = System.currentTimeMillis();insertionSort(insertionArray);long insertionTime = System.currentTimeMillis() - start;// 测试快速排序int[] quickArray = original.clone();start = System.currentTimeMillis();quickSort(quickArray);long quickTime = System.currentTimeMillis() - start;// 测试Arrays.sort()(优化的快速排序)int[] arraysArray = original.clone();start = System.currentTimeMillis();Arrays.sort(arraysArray);long arraysTime = System.currentTimeMillis() - start;System.out.printf("冒泡排序:%d ms%n", bubbleTime);System.out.printf("选择排序:%d ms%n", selectionTime);System.out.printf("插入排序:%d ms%n", insertionTime);System.out.printf("快速排序:%d ms%n", quickTime);System.out.printf("Arrays.sort():%d ms%n", arraysTime);}
}private static int[] generateRandomArray(int size) {Random random = new Random();int[] arr = new int[size];for (int i = 0; i < size; i++) {arr[i] = random.nextInt(1000); // 0-999的随机数}return arr;
}
7. Arrays工具类深度应用
java.util.Arrays
类提供了丰富的数组操作方法,在实际开发中使用频率很高。
7.1 常用方法详解
数组比较方法
public class ArraysUtilDemo {public static void demonstrateComparison() {int[] arr1 = {1, 2, 3, 4, 5};int[] arr2 = {1, 2, 3, 4, 5};int[] arr3 = {1, 2, 3, 5, 4};// 错误的比较方式:比较引用地址System.out.println("arr1 == arr2: " + (arr1 == arr2)); // false// 正确的比较方式:比较内容System.out.println("Arrays.equals(arr1, arr2): " + Arrays.equals(arr1, arr2)); // trueSystem.out.println("Arrays.equals(arr1, arr3): " + Arrays.equals(arr1, arr3)); // false// 多维数组比较int[][] matrix1 = {{1, 2}, {3, 4}};int[][] matrix2 = {{1, 2}, {3, 4}};System.out.println("Arrays.equals(matrix1, matrix2): " + Arrays.equals(matrix1, matrix2)); // false!System.out.println("Arrays.deepEquals(matrix1, matrix2): " + Arrays.deepEquals(matrix1, matrix2)); // true}
}
数组转字符串方法
public static void demonstrateToString() {int[] numbers = {1, 2, 3, 4, 5};// 错误的输出方式System.out.println("直接输出:" + numbers); // 输出类似:[I@1b6d3586(内存地址)// 正确的输出方式System.out.println("Arrays.toString():" + Arrays.toString(numbers));// 输出:[1, 2, 3, 4, 5]// 多维数组的字符串转换int[][] matrix = {{1, 2, 3}, {4, 5, 6}};System.out.println("Arrays.toString(matrix):" + Arrays.toString(matrix));// 输出:[[I@..., [I@...] (仍然是地址)System.out.println("Arrays.deepToString(matrix):" + Arrays.deepToString(matrix));// 输出:[[1, 2, 3], [4, 5, 6]]
}
数组填充方法
public static void demonstrateFill() {// 填充整个数组int[] arr = new int[5];Arrays.fill(arr, 42);System.out.println("填充42:" + Arrays.toString(arr)); // [42, 42, 42, 42, 42]// 填充部分数组int[] arr2 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};Arrays.fill(arr2, 2, 7, 0); // 从索引2到6(不包括7)填充0System.out.println("部分填充:" + Arrays.toString(arr2)); // [1, 2, 0, 0, 0, 0, 0, 8, 9, 10]// 实际应用:重置数组boolean[] flags = new boolean[10];Arrays.fill(flags, true); // 全部设为trueArrays.fill(flags, 0, 3, false); // 前3个设为falseSystem.out.println("布尔数组:" + Arrays.toString(flags));// [false, false, false, true, true, true, true, true, true, true]
}
数组排序方法
public static void demonstrateSort() {// 基本排序int[] numbers = {64, 34, 25, 12, 22, 11, 90};System.out.println("排序前:" + Arrays.toString(numbers));Arrays.sort(numbers);System.out.println("排序后:" + Arrays.toString(numbers));// 部分排序int[] partialSort = {64, 34, 25, 12, 22, 11, 90};Arrays.sort(partialSort, 1, 5); // 只排序索引1-4的元素System.out.println("部分排序:" + Arrays.toString(partialSort));// [64, 12, 22, 25, 34, 11, 90]// 字符串排序String[] names = {"张三", "李四", "王五", "赵六"};Arrays.sort(names);System.out.println("姓名排序:" + Arrays.toString(names));// 自定义排序(降序)Integer[] nums = {64, 34, 25, 12, 22, 11, 90};Arrays.sort(nums, Collections.reverseOrder());System.out.println("降序排序:" + Arrays.toString(nums));// 按长度排序字符串String[] words = {"apple", "pie", "washington", "book"};Arrays.sort(words, (a, b) -> a.length() - b.length());System.out.println("按长度排序:" + Arrays.toString(words));// [pie, book, apple, washington]
}
数组搜索方法
public static void demonstrateSearch() {int[] sortedArray = {10, 20, 30, 40, 50, 60, 70, 80, 90};// 二分查找:返回索引int index = Arrays.binarySearch(sortedArray, 50);System.out.println("找到50在索引:" + index); // 4// 查找不存在的元素int notFound = Arrays.binarySearch(sortedArray, 25);System.out.println("查找25的结果:" + notFound); // 负数,表示插入点if (notFound < 0) {int insertPoint = -notFound - 1;System.out.println("25应该插入在索引:" + insertPoint); // 2}// 在部分数组中搜索int partialSearch = Arrays.binarySearch(sortedArray, 2, 6, 40);System.out.println("在索引2-5中找40:" + partialSearch); // 3// 字符串搜索String[] names = {"Alice", "Bob", "Charlie", "David", "Eve"};int nameIndex = Arrays.binarySearch(names, "Charlie");System.out.println("找到Charlie在索引:" + nameIndex); // 2
}
7.2 高级应用技巧
数组拷贝的多种方式对比
public static void demonstrateCopyMethods() {int[] original = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};// 1. Arrays.copyOf() - 复制指定长度int[] copy1 = Arrays.copyOf(original, 5); // 只复制前5个System.out.println("copyOf(5):" + Arrays.toString(copy1));// [1, 2, 3, 4, 5]int[] copy2 = Arrays.copyOf(original, 15); // 长度超过原数组System.out.println("copyOf(15):" + Arrays.toString(copy2));// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0, 0, 0, 0, 0]// 2. Arrays.copyOfRange() - 复制指定范围int[] copy3 = Arrays.copyOfRange(original, 2, 7); // 索引2-6System.out.println("copyOfRange(2,7):" + Arrays.toString(copy3));// [3, 4, 5, 6, 7]// 3. System.arraycopy() - 最灵活的复制int[] copy4 = new int[8];System.arraycopy(original, 1, copy4, 2, 5); // 从original[1]复制5个到copy4[2]System.out.println("System.arraycopy():" + Arrays.toString(copy4));// [0, 0, 2, 3, 4, 5, 6, 0]
}
数组类型转换技巧
public static void demonstrateArrayConversion() {// 基本类型数组转包装类型int[] primitiveArray = {1, 2, 3, 4, 5};Integer[] wrapperArray = Arrays.stream(primitiveArray).boxed().toArray(Integer[]::new);System.out.println("转为包装类型:" + Arrays.toString(wrapperArray));// 包装类型转基本类型Integer[] integers = {10, 20, 30, 40, 50};int[] primitives = Arrays.stream(integers).mapToInt(Integer::intValue).toArray();System.out.println("转为基本类型:" + Arrays.toString(primitives));// 字符串数组转整数数组String[] stringNumbers = {"100", "200", "300", "400"};int[] numbers = Arrays.stream(stringNumbers).mapToInt(Integer::parseInt).toArray();System.out.println("字符串转数字:" + Arrays.toString(numbers));// 数组转ListString[] names = {"Alice", "Bob", "Charlie"};List<String> nameList = Arrays.asList(names);System.out.println("数组转List:" + nameList);// 注意:Arrays.asList()返回的List不能修改大小try {nameList.add("David"); // 会抛出UnsupportedOperationException} catch (UnsupportedOperationException e) {System.out.println("Arrays.asList()返回的List不支持添加元素");}// 如果需要可修改的ListList<String> mutableList = new ArrayList<>(Arrays.asList(names));mutableList.add("David");System.out.println("可修改的List:" + mutableList);
}
数组作为Java编程的基石,其重要性不言而喻。掌握好数组操作,不仅能提高编程效率,还能为学习更高级的数据结构和算法打下坚实基础。希望这篇指南能够帮助你彻底理解和熟练运用Java数组!