第四章 数组

【本章主要内容】

  • 数组的概念和初始化
  • 数组的常见算法及应用
  • 数组工具类的使用及常见异常

一、概述

1、什么是数组

在Java中,数组 (Array) 是一个用于存储固定数量相同类型 元素的容器。通过一个统一的数组名 来引用这组数据,并通过从0开始的索引 (Index) 来访问其中的每一个元素 (Element)

  • 数组名: 通过一个变量名对一组数据进行统一命名,这个变量名被称为数组名
  • 下标或索引:通过编号的方式对这些数据进行使用和管理,这个编号被称为下标或索引(从 0 开始)。
  • 元素:数组中的每一个数据称为元素(Element)。
  • 长度:数组中元素的总个数被称为数组的长度(Length)。

2、分类

  • 基本数据类型数组是指数组元素是 8 种基本数据类型的值
  • 引用数据类型数组 是指数组元素中存储的是对象 ,也称为对象数组

3、特点

  • 类型统一:一个数组中所有元素的数据类型必须完全相同。
  • 长度固定:数组一旦被创建,其长度 (Length) 就不可改变。
  • 内存连续:数组在内存中会开辟一整块连续的空间,数组中的元素在内存中是依次紧密排列的,有序的。这使得通过索引访问元素的速度非常快。
  • 引用类型:数组名本身是一个引用变量,它指向堆内存中数组对象的首地址。

二、一维数组

1、声明

java 复制代码
数据类型[] 数组名;   // 首选的方法
或
数据类型 数组名[];  // 效果相同,但不是首选方法,来自 C/C++ 语言 ,在Java中采用是为了让 C/C++ 程序员能够快速理解java语言。

【注意】: 数组声明的明确事项

  1. 数组的维度:在 Java 中数组的符号是[],[]表示一维,[] []表示二维。
  2. 数组的元素类型 :dataType,即创建的数组容器可以存储什么数据类型的数据。元素的类型可以是任意的 Java 的数据类型。例如:int、String、Student 等。
  3. 数组名:arrayRefVar 就是代表某个数组的标识符,数组名其实也是变量名,按照变量的命名规范来命名。数组名是个引用数据类型的变量,因为它代表一组数据。

2、初始化

所谓的初始化是指确定数组的长度和元素的值。因为只有确定了数组的长度,才能为数组开辟对应大小的内存空间。数组的初始化可以分为静态初始化和动态初始化。

(1)静态初始化

静态初始化:在声明时就直接指定数组中包含的元素,由编译器自动计算数组长度。

静态初始化有两种格式,下面分别对两种格式进行说明(合并了声明和初始化)。

java 复制代码
// 格式一,完整格式
数据类型[] 数组名 = new 数据类型[]{元素1,元素2...,元素k}; 
//格式二,常用,Java的类型推断机制,简化了代码的书写。
数据类型[] 数组名 = {元素1,元素2...,元素k}; 

// 注意:简化格式必须在一条语句中完成声明和初始化,以下写法是错误的!
// int[] arr3;
// arr3 = {10, 20, 30, 40}; // 编译错误

【注意】

  1. 在以上两种语法格式中,要求大括号中的各元素用逗号隔开。
  2. 元素可以重复,元素个数也可以是零个或多个。
  3. 格式 2 是格式 1 的简化,二者没有本质的区别。
  4. 格式 2 不能像格式 1 先声明再进行初始化,必须在一个语句中完成,如下所示的代码就会报错:

(2)动态初始化

动态初始化:在声明时只指定数组的长度,这也是动态初始化与静态初始化的主要区别。数组中每个元素的值由系统根据其类型赋予默认值。

动态初始化的语法格式如下所示(合并了声明和初始化)。

java 复制代码
数据类型[] 数组名 = new 数据类型[数组长度];

// 例如:动态初始化一个长度为5的int数组
int[] arr = new int[5];

【注意】

  1. 数组长度:表示数组可以容纳的元素数量。必须是一个整数表达式,它可以是一个整数常量、一个整数变量,或者是任何返回整数值的表达式。
  2. 数组有定长特性,长度一旦指定,不可更改。
  3. 数组的索引 将从 0 开始,到 数组长度 - 1 结束。

【元素的默认值】

动态初始化后,由于没有指定数组元素,数组元素会自动获得默认值。

3、访问与遍历

(1)元素访问

java 复制代码
public class Array {
    public static void main(String[] args) {
        // 创建数组
        int[] arr = {1, 2, 3, 4, 5};
        // 输出数组长度和第一个元素
        System.out.println(arr.length);
        System.out.println(arr[0]);
        // 修改第一个元素
        arr[0] = 10;
        // 输出修改后的第一个元素
        System.out.println(arr[0]);
        // 获取数组的最后一个元素
        System.out.println(arr[arr.length - 1]);
        // 尝试访问超出数组长度的元素
        System.out.println(arr[arr.length]); // 报错:索引越界异常:java.lang.ArrayIndexOutOfBoundsException: 5  
    }
}

(2)数组遍历

数组遍历就是对数组中的元素按顺序依次进行访问。

java 复制代码
// 传统遍历
class ArrayElementl {
    public static void main(String[] args) {
        // 创建数组
        int[] arr = {10, 20, 30};
        // 遍历数组
        System.out.println("数组的第一个元素是:" + arr[0]);
        System.out.println("数组的第二个元素是:" + arr[1]);
        System.out.println("数组的第三个元素是:" + arr[2]);
    }
}

// for循环遍历
// 创建数组
int[] arr = {10, 20, 30};
	for (int i = 0; i < arr.length; i++) {
		System.out.println("数组的第" + (i + 1) + "个元素是:" + arr[i]);
}
    
// 增强 for 循环 (for-each)遍历
int[] arr = {1, 2, 3, 4, 5};
for (int element : arr) {
    System.out.println("元素值为: " + element);
}

4、内存分析

(1)连续的内存空间

当一个数组被初始化时,Java虚拟机(JVM)会在堆内存 中为其分配一整块连续的存储空间

  • 空间大小 :系统根据数组的元素类型确定每个存储单元的大小(例如,int类型占4个字节),再根据数组长度决定开辟多少个这样的单元格,并将元素依次放在对应的空间中。
  • 数组引用 :我们使用的数组名(例如 arr)是一个引用变量,它存储的正是这块连续内存空间的首地址

(2)寻址原理:首地址与偏移量

  • 定位方式 ​:知道了首地址,就可以通过索引(下标)计算出​偏移量​,从而快速定位到任何一个元素。

    • 访问第一个元素 arr[0],就是访问首地址本身(偏移量为0)。
    • 访问第二个元素 arr[1],就是在首地址的基础上偏移一个元素的存储宽度。
  • 形象比喻:这个过程就像和朋友们住宾馆,房间都是挨着的。只要找到了第一个房间(首地址),根据房间号的顺序(索引),就能立刻找到其他任何一个房间。

虽然数组名变量中确实存储了地址信息,但Java出于安全和抽象的考虑,并不会直接向用户暴露真实的内存地址。

当我们尝试直接打印数组名(如 System.out.println(arr);​),控制台显示的并不是内存地址值,而是一串代表对象信息的字符串。形如 [I@7a81197d​,其含义是:

  • [ : 代表这是一个数组。
  • I : 代表数组的元素类型是 int
  • @ : 分隔符。
  • 7a81197d : 这是对象的哈希码 (Hash Code) ,它由内存地址计算得来,可以看作是对象的唯一标识,但并非地址本身。

三、数组的赋值与复制

在Java中,变量的赋值行为取决于其数据类型。通过下面的代码,我们可以清晰地看到基本数据类型和引用数据类型(如数组)在赋值时的根本不同。

java 复制代码
public class AssignmentTest {
    public static void main(String[] args) {
        // --- 基本数据类型:值拷贝 ---
        int a = 1;
        int b = a; // 将a的值复制一份给b
        System.out.println("a = " + a + ", b = " + b); // a = 1, b = 1
        
        b = 2; // 修改b的值
        System.out.println("修改后, a = " + a + ", b = " + b); // a = 1, b = 2 (a未受影响)

        // --- 引用数据类型(数组):引用地址值拷贝 ---
        int[] arr1 = {2, 3, 5, 7, 11};
        int[] arr2 = arr1; // 将arr1的引用地址值复制给arr2

        System.out.println("arr1: " + java.util.Arrays.toString(arr1)); // arr1: [2, 3, 5, 7, 11]
        System.out.println("arr2: " + java.util.Arrays.toString(arr2)); // arr2: [2, 3, 5, 7, 11]

        arr2[0] = 0; // 通过arr2修改数组元素
        System.out.println("修改后, arr1: " + java.util.Arrays.toString(arr1)); // arr1: [0, 3, 5, 7, 11] (arr1受到影响)
        System.out.println("修改后, arr2: " + java.util.Arrays.toString(arr2)); // arr2: [0, 3, 5, 7, 11]
    }
}

(注:为了更清晰地展示数组内容,代码中使用了 java.util.Arrays.toString()方法。)

1、基本数据类型:值的复制

当执行 int b = a;​ 时,Java进行了​值拷贝​。

  • 内存行为 :系统会在栈内存中为变量 b 开辟一块新的、独立的空间 ,然后将变量 a 中存储的具体数值 (1) 复制到 b 的空间中。
  • 结果ab 是两个完全独立的变量,各自持有自己的数据。因此,修改其中一个变量的值,对另一个变量毫无影响

2、数组(引用类型):地址值的复制

当执行 int[] arr2 = arr1;​ 时,Java进行了​引用拷贝​。

  • 内存行为 :数组是引用类型,其实际数据(元素 1, 2, 3)存储在堆内存 中。变量
    arr1 本身在栈内存 中并不存储这些数据,而是存储了该数组对象在堆中的内存地址
    这句赋值语句的本质是:将 arr1 变量中所存储的地址值 ,复制一份给了 arr2
  • 结果arr1arr2 这两个引用变量,现在指向了堆内存中同一个数组对象。它们就像是同一间房子的两把钥匙。无论用哪把钥匙开门进去修改了房间里的东西,另一把钥匙下次开门时看到的也必然是修改后的结果。

3、结论

  • 基本数据类型 进行赋值操作,是值的复制
  • 引用数据类型 (包括数组、所有对象)进行赋值操作,是引用的复制(即地址的复制)。

四、数组算法

1、数组元素特征值

数组元素的特征值统计是非常基础的算法,常见的操作有统计数组中满足某特征元素的个数、求元素的最值、平均值、总和等。

(1)求总和、均值

java 复制代码
// 求总和、均值
public class TestArraySum {
    public static void main(String[] args) {
        int[] arr = {4,5,6,1,9};
        int sum = 0; // 因为0加上任何数都不影响结果
        for(int i=0;i<arr.length;i++){
            sum += arr[i];
        }
        System.out.println("总和为:" + sum);
        double mean = (double)sum/arr.length;
        System.out.println("均值为:" + mean);
    }
}

(2)求总乘积

java 复制代码
// 求总乘积
public class TestArrayMul {
    public static void main(String[] args) {
        int[] arr = {4,5,6,1,9};
        long result = 1; // 因为1乘以任何数都不影响结果
        for(int i=0; i<arr.length; i++){
            result *= arr[i];
        }
        System.out.println("总乘积为: " + result);
    }
}

2、数组元素反转

实现思想: 数组对称位置的元素互换。

方法一:使用 for​ 循环原地反转数组

这是最直接的方法,通过交换数组两端的元素来实现反转。

  1. 确定交换几次 次数 = 数组.length / 2

  2. 谁和谁交换

    java 复制代码
    for(int i=0; i< arr.length / 2; i++){
             int temp = arr[i]; // 使用 temp 临时变量存储 arr1[i] 的值
             arr[i] = arr[arr.length-1-i]; // 将数组末尾的元素 arr1[arr.length - 1 - i] 赋值给 arr1[i]
             arr[arr.length-1-i] = temp; // 将 temp 中存储的原始 arr1[i] 值赋值给 arr1[arr - 1 - i],完成交换。
        }

【举例】

java 复制代码
int[] arr1 = {1, 2, 3, 4, 5}; // 原数组

int n = arr1.length; // 获取数组长度

// 使用 for 循环反转的核心代码
for (int i = 0; i < n / 2; i++) {
    int temp = arr1[i];
    arr1[i] = arr1[n - 1 - i];
    arr1[n - 1 - i] = temp;
}

// 打印反转后的数组
for (int i = 0; i < arr1.length; i++) {
    System.out.print(arr1[i] + " ");
}

方法二:使用辅助数组

这种方法通过创建一个新的数组,并将原数组的元素从后向前复制到新数组中来实现反转。不过此时,相当于堆中有两个数组,在内存中占了 2 倍的空间。

【举例】

java 复制代码
int[] arr1 = {1, 2, 3, 4, 5}; // 原数组
int[] arr2 = new int[arr1.length]; // 创建一个新的数组用于存储反转后的元素,其长度与 arr1 相同

for (int i = 0; i < arr1.length; i++) {
    arr2[i] = arr1[arr1.length - 1 - i]; // 将原数组 arr1 的元素从后向前依次复制到新数组 arr2
}

// 打印反转后的新数组
for (int i = 0; i < arr2.length; i++) {
    System.out.print(arr2[i] + " ");
}

4、数组元素排序算法

(1)排序的基本概念

1. 定义

排序,就是将一组记录(如数组中的元素)按照其关键字(通常是元素值本身)的某种顺序(如升序或降序)进行重新排列的过程。

2. 目的

排序的主要目的之一是为了后续的高效查找。例如,在有序数组中使用二分查找,其效率远高于在无序数组中的线性查找。

3. 分类

  • 内部排序:所有排序操作都在内存中完成。适用于数据量不大,可以一次性载入内存的场景。我们通常接触的都是内部排序。
  • 外部排序:当数据量巨大,无法一次性全部加载到内存时,需要借助磁盘等外部存储进行排序。外部排序通常是内部排序的延伸和组合。

(2)衡量算法的标尺

我们通过以下三个核心指标来评估一个排序算法的优劣:

1. 时间复杂度 (Time Complexity)

  • 概念 :它衡量的是算法的执行时间随数据规模 n​ 增长而变化的趋势,通常用大O表示法 O(f(n))​ 来表示。时间复杂度关心的是增长率,而不是精确的执行时间。

  • 常见复杂度(从优到劣)O(1) < O(log n) < O(n) < O(n log n) < O(n ​**2) < O(n 3) < O(2 n**​ )

    (注:在算法领域,log n通常指以2为底的对数)

2. 空间复杂度 (Space Complexity)

  • 概念:它衡量的是算法在运行过程中所需要消耗的额外存储空间。我们关注的是除了存储原始数据外,算法本身需要多少辅助内存。

3. 稳定性 (Stability)

  • 概念 :如果待排序的序列中有两个相等的元素,在排序后,它们原来的相对前后顺序保持不变,则称该排序算法是稳定 的;反之,则为不稳定的。

(4)常见内部排序算法

排序算法 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性
冒泡排序 O(n2) O(n2) O(1) 稳定
选择排序 O(n2) O(n2) O(1) 不稳定
插入排序 O(n2) O(n2) O(1) 稳定
快速排序 O(n log n) O(n2) O(log n) 不稳定
归并排序 O(n log n) O(n log n) O(n) 稳定
堆排序 O(n log n) O(n log n) O(1) 不稳定

如何选择?

  1. n非常小 (如 n ≤ 50)

    • 可选用直接插入排序直接选择排序
    • 如果数据基本有序,插入排序表现更佳。
  2. n较大

    • 应采用时间复杂度为 O(n log n) 的排序方法,如快速排序堆排序归并排序
    • 快速排序:平均性能最佳,是实际应用中最常用的排序算法之一。
    • 归并排序:性能稳定,时间复杂度始终为 O(n log n),且是稳定排序,常用于需要稳定性的场景。
    • 堆排序:性能同样稳定,且空间复杂度优于归并排序。

(5)冒泡排序(BubbleSort)

  • 核心思想:每一轮通过相邻元素的比较和交换,将当前未排序区间的最大(或最小)元素像气泡一样逐渐"冒"到序列的末尾。

  • 性能评估

    • 时间复杂度:O(n2)
    • 空间复杂度:O(1)
    • 稳定性:稳定,相邻的相等元素不进行交换,保持了它们的原始相对顺序
  • 执行步骤

    1. 外层循环控制总轮数,共进行 n-1 轮。
    2. 内层循环负责在当前轮次中,从头到尾比较相邻的两个元素。
    3. 如果前一个元素大于后一个元素,则交换它们的位置。
    4. 一轮结束后,未排序区间的最大值就被放置到了正确的位置。
  • 步骤示意

*(动态演示: visualgo.net/zh/sorting*​

  • 代码示例
java 复制代码
/**
 * 冒泡排序示例:
 * 通过不断比较相邻元素,把较大的"冒"到右侧,最终得到一个从小到大的有序数组。
 */
public class BubbleSortDemo {

    public static void main(String[] args) {
		// 定义并初始化一个待排序数组
        int[] arr = {6, 9, 2, 9, 1};  
		// 记录数组长度,避免在循环里多次调用 arr.length                   
        int n = arr.length;                              

        /* 外层循环:决定需要进行多少轮"冒泡"
         *    • 第 1 轮结束后,最大的元素被挤到最右端
         *    • 第 2 轮结束后,次大的也就位
         *    • ......
         *    • 理论最多进行 n-1 轮即可
         */
        for (int i = 1; i < n; i++) {
			// flag 用来标记"本轮是否发生过交换",若整轮都没交换,说明数组已完全有序,可提前终止
            boolean flag = false; 

            /* 内层循环:在当前尚未排好序的区间内做相邻元素比较
             *    此时右侧已有 i 个元素确定,所以 j 只需遍历到 n-i-1(所以范围就是j < n - i)
             */
            for (int j = 0; j < n - i; j++) {
				// 若前一个元素比后一个大,则顺序不对,需要交换
                if (arr[j] > arr[j + 1]) {               

                    int temp = arr[j];      // (1) 把较大的元素暂存
                    arr[j] = arr[j + 1];    // (2) 把较小的元素前移
                    arr[j + 1] = temp;      // (3) 把较大的元素后移

                    flag = true;            // 记录:本轮确实发生过交换
                }
            }
			// 如果一整轮都没交换,说明数组已是升序,直接跳出外层循环
            if (!flag) {                                 
                break;
            }
        }

        System.out.println("排序后的数组:");              
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + "  ");
        }
    }
}

(6)选择排序(SelectionSort)

  • 核心思想 :每一轮从当前的未排序区间中"选择"出最小(或最大)的元素,并将其与未排序区间的第一个元素交换位置。

  • 性能评估

    • 时间复杂度:O(n2)
    • 空间复杂度:O(1)
    • 稳定性:不稳定,找到最小(或最大)元素后与前方元素交换时,可能改变相等元素的相对顺序。
  • 执行步骤

    1. 外层循环控制轮数,共进行 n-1 轮。每一轮确定一个位置上的正确元素。
    2. 在每一轮中,假设未排序区间的第一个元素为最小,记录其索引。
    3. 内层循环遍历未排序区间,寻找实际最小元素的索引。
    4. 一轮内循环结束后,将找到的实际最小元素与未排序区间的第一个元素进行交换。
  • 步骤示意:

  • 代码示例
java 复制代码
/**
 * 选择排序示例:
 * 每一轮从未排序区间选择最小的元素,放到已排序区间的末尾。
 */
public class TestSelectionSort {

    public static void main(String[] args) {
		// 定义并初始化一个待排序数组
        int[] arr = {6, 9, 2, 9, 1}; 
		// 记录数组长度                    
        int n = arr.length;                             

        /* 外层循环:决定需要进行多少轮选择
         *    • i 代表当前轮次中,最小元素应该被放置到的目标位置的索引
         *    • 总共需要进行 n-1 轮
         */
        for (int i = 0; i < n - 1; i++) {
            int minIndex = i; // 假设本轮未排序部分的第一个元素是最小的

            /* 内层循环:在 arr[i+1...n-1] 这个未排序区间中找到最小元素的索引
             */
            for (int j = i + 1; j < n; j++) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j; // 更新最小元素的索引
                }
            }

            // 如果最小元素的索引不是当前轮次的起始位置i,则交换
            if (minIndex != i) {
                int temp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = temp;
            }
        }

        System.out.println("排序后的数组:");              
        for (int k = 0; k < arr.length; k++) {
            System.out.print(arr[k] + "  ");
        }
    }
}

(7)插入排序(InsertionSort)

  • 核心思想 :将整个数组看作已排序未排序两部分。每一轮从未排序部分取出一个元素,将其插入到已排序部分的正确位置,以维持已排序部分的有序性。就像打扑克牌时整理手中的牌。

  • 性能评估

    • 时间复杂度:O(n2) (但在近乎有序的数组上效率很高,接近O(n))
    • 空间复杂度:O(1)
    • 稳定性:稳定
  • 执行步骤

    1. 默认数组第一个元素构成初始的已排序区间。
    2. 外层循环从未排序区间的第一个元素(即数组第二个元素)开始,逐个取出作为"待插入元素"。
    3. 内层循环(或while循环)将"待插入元素"与已排序区间的元素从后往前比较。
    4. 若已排序元素大于"待插入元素",则将该元素后移一位。
    5. 重复步骤4,直到找到插入位置(即遇到一个小于或等于"待插入元素"的已排序元素,或已到达区间头部),将"待插入元素"放入该位置。
  • 步骤示意:

  • 代码示例:
java 复制代码
/**
 * 插入排序示例:
 * 将数组分为已排序和未排序两部分,每次从未排序部分取一个元素插入到已排序部分的正确位置。
 */
public class InsertionSort {

    public static void main(String[] args) {
		// 定义并初始化一个待排序数组
        int[] arr = {6, 9, 2, 9, 1};    
		// 记录数组长度                 
        int n = arr.length;                              

        /* 外层循环:从第二个元素开始(索引为1),逐个选取元素作为待插入元素
         *    i 指向当前待插入元素的索引
         */
        for (int i = 1; i < n; i++) {
            int currentElement = arr[i]; // 当前需要被插入到前面有序序列中的元素
            int j = i - 1;               // j 是指向已排序序列中最后一个元素的索引

            /* 内层循环 (或者说是一个查找和移动的过程):
             *    将 currentElement 与已排序序列中的元素从后向前比较
             *    如果已排序序列中的元素 arr[j] 大于 currentElement,
             *    则将 arr[j] 向后移动一位,为 currentElement 腾出空间
             */
            while (j >= 0 && arr[j] > currentElement) {
                arr[j + 1] = arr[j]; // 元素后移
                j--;                 // 继续向前比较
            }
            // 当循环结束时,j+1 就是 currentElement 应该插入的位置
            // (因为 j 要么变成了 -1,要么 arr[j] <= currentElement)
            arr[j + 1] = currentElement; // 插入元素
        }

        System.out.println("排序后的数组:");              
        for (int k = 0; k < arr.length; k++) {
            System.out.print(arr[k] + "  ");
        }
    }
}

5、数组元素查找算法

(1)线性查找 (Linear Search)

也称为顺序查找,是最基础的查找算法。

  • 核心思想:从数组的第一个元素开始,逐个向后扫描,将其与目标值进行比较。

  • 适用场景 :适用于任何数组,无论其是否有序。

  • 执行过程

    1. 如果当前元素与目标值相等,查找成功,返回当前元素的索引。
    2. 如果扫描完整个数组仍未找到,则查找失败。
java 复制代码
public class TestArrayOrderSearch {
    //查找value第一次在数组中出现的index
    public static void main(String[] args){
        int[] arr = {4,5,6,1,9};
        int value = 1; //要查找的元素
        int index = -1; //下标一开始初始化为-1,因为正常的下标不会是-1,所以如果最后index的值仍然是-1,那么说明要查找的元素不在此数组中

        for(int i=0; i<arr.length; i++){ 
            if(arr[i] == value){  
                index = i;  
                break;  
            }   
        }   
        //输出结果
        if(index==-1){  
            System.out.println(value + "不存在");  
        }else{  
            System.out.println(value + "的下标是" + index);
        }
    }
}

(2)二分查找 (Binary Search)

  • 核心前提数组必须是有序的。这是使用二分查找的绝对前提。

  • 核心思想:通过不断将查找范围对半分割,来快速定位目标元素。

  • 执行过程

    1. 定义三个指针:head​(头部)、tail​(尾部)和 mid​(中间)。

    2. 比较 arr[mid]​ 与目标值:

      • 若相等,则查找成功。
      • arr[mid] 小于目标值,说明目标在右半区,将 head 移至 mid + 1
      • arr[mid] 大于目标值,说明目标在左半区,将 tail 移至 mid - 1
    3. 重复步骤2,直到 head > tail​,表示查找范围为空,查找失败。

java 复制代码
/**
 * 二分查找示例:
 * 在一个有序数组中高效地查找目标值。
 */
public class TestBinarySearch {
    public static void main(String[] args) {
        int[] arr = new int[]{-99, -54, -2, 0, 2, 33, 43, 256, 999};  
  
        boolean isFlag = false; // 标记是否找到目标值,找到就设置为true
        int target = 256;        // 目标值
        int head = 0;           // 头索引
        int tail = arr.length - 1; // 尾索引

		// 循环条件: 头索引小于等于尾索引,因为当头索引大于尾索引时,说明数组中不存在目标值,此时循环结束
        while (head <= tail) {  
			// 计算中间索引,head + (tail - head) 防止(tail+head) 溢出
            int mid = head + (tail - head) / 2;    
            if (arr[mid] == target) {
                System.out.println("找到目标值, 索引为: " + mid);
                isFlag = true;
                break;
            } else if (arr[mid] < target) {  // 目标值在右半部分,更新左边界
                head = mid + 1;
            } else {  // 目标值在左半部分,更新右边界 (arr[mid] > target)
                tail = mid - 1;
            }
        }
        if (!isFlag) {
            System.out.println("未找到目标值: " + target);
        }
    }
}

(3)查找最值

  • 核心思想:采用"打擂台"的方式。先假设数组第一个元素是最大(或最小)值,然后遍历数组的其余元素,逐个与当前的"擂主"比较,如果挑战者更优,则更新"擂主"。
java 复制代码
// 求最值及最值出现的下标
public class TestArrayExtrema {
    public static void main(String[] args) {
        int[] arr = {4,5,6,1,9,9,3};
        //找最大值
        int max = arr[0]; // 将变量 max 初始化为数组的第一个元素 arr[0]
        for(int i=1; i<arr.length; i++){ // 循环查找最大值,此处i从1开始,是max不需要与arr[0]再比较一次了
            if(arr[i] > max){
                max = arr[i];
            }
        }
        System.out.println("最大值是:" + max);
        System.out.print("最大值的下标有:");   // 表示接下来将输出最大值的下标。

        //遍历数组,看哪些元素和最大值是一样的
        for(int i=0; i<arr.length; i++){
            if(max == arr[i]){
                System.out.print(i+"\t"); // 用制表符 \t 分隔
            }
        }
        System.out.println();

        // 找最小值
        int min = arr[0];
        for(int i=1; i<arr.length; i++){
            if(arr[i] < min){
                min = arr[i];
            }
        }
        System.out.println("最小值是:" + min);
        System.out.print("最小值的下标有:");

        //遍历数组,看哪些元素和最小值是一样的
        for(int i=0; i<arr.length; i++){
            if(min == arr[i]){
                System.out.print(i+"\t");
            }
        }    
    }
}

6、Arrays 工具类的使用

在实际的后端开发中,我们很少自己从头编写排序、查找等基础算法。JDK在java.util.Arrays​类中为我们提供了经过高度优化的、可以直接使用的静态方法。

java 复制代码
import java.util.Arrays;

public class ArraysUtilDemo {
    public static void main(String[] args) {
        int[] arr = {3, 2, 5, 1, 6};
        System.out.println("排序前: " + Arrays.toString(arr)); // 使用toString()打印
        
        Arrays.sort(arr); // 使用sort()排序
        
        System.out.println("排序后: " + Arrays.toString(arr));
    }
}

7、数组中的常见异常

(1)ArrayIndexOutOfBoundsException(数组索引越界异常)

  • 触发原因:访问了不存在的索引。数组的合法索引范围是 [0, array.length - 1]。任何超出这个范围的访问都会触发此异常。
  • 开发者法则:这不是一个需要用 try-catch 捕获的异常,而是一个必须修复的编码Bug。
java 复制代码
public class ArrayIndexOutOfBoundsDemo {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        
        // 正确访问最后一个元素
        System.out.println("最后一个元素:" + arr[arr.length - 1]); // 输出: 3

        // 错误示例:以下两行都会抛出 ArrayIndexOutOfBoundsException
        // System.out.println(arr[3]); 
        // System.out.println(arr[arr.length]); 
    }
}

(2)NullPointerException(空指针异常)

  • 触发原因 :当一个引用变量的值为 null​ 时,尝试通过它去调用方法或访问属性(如 length​)。null​ 表示"没有指向任何对象",自然无法进行任何操作。

  • 常见场景

    1. 一个数组变量被声明但从未被 new 初始化。
    2. 对于对象数组,数组本身已初始化,但其内部元素为 null
    3. 对于二维数组,只初始化了外层数组,而内层数组为 null

观察以下代码

java 复制代码
public class NullPointerDemo {
    public static void main(String[] args) {
        // 这是导致空指针异常的典型场景(针对二维数组)
        int[][] arr = new int[3][]; // 只创建了外层数组,arr[0], arr[1], arr[2] 的值都是 null

        // arr[0] 的值是 null,尝试在 null 上访问索引 [0],必然导致空指针异常
        System.out.println(arr[0][0]); // 触发 NullPointerException
    }
}

五、多维数组

当要存储一组数据时,可以考虑使用一维数组;那么当有多组数据需要存储和处理时,就需要用到多维数组。一般来讲,二维数组就已经满足了很多场景下的需求。

二维数组实际上就是一维数组作为元素构成的新数组,里面的每个元素都是一个一维数组。我们往往将二维数组中一维数组的个数称为行数,将每个一维数组的元素个数称为列数。

在 Java 中,二维数组不一定是规则的矩阵,即每个一维数组的列数不一定一样。从数组底层的运行机制来看,其实没有多维数组。

1、声明

java 复制代码
// 推荐方式
数组类型[][] 数组名;

// 其他等价方式
数组类型 数组名[][];
数组类型[] 数组名[];

2、初始化

(1)静态初始化

java 复制代码
// 格式一
数据类型[][] 数组名 = new 数据类型[][]{{元素1,元素2...},{元素1,元素2...},{元素1,元素2...}}; 
// 格式二,简化版
数据类型[][] 数组名 = {{元素1,元素2...},{元素1,元素2...},{元素1,元素2...}}; 

(2)动态初始化

所谓动态初始化,是指在对数组初始化时,只是确定数组的行数和列数,甚至行数和列数都需要在程序运行期间才能确定。当确定完数组的行数和列数之后,数组的元素是默认值。

动态初始化分为两种,一种是每行的列数可以相同,另一种是每行的列数可以不同。

  • 规则数组 (每一行列数相同)
java 复制代码
//(1)确定行数和列数
//此时创建完数组,行数、列数确定,而且元素也都有默认值
数据类型[] []  二维数组名 = new 数据类型[m] [n]; 
//(2)再为元素赋新值
二维数组名[行下标] [列下标] = 值;

m:表示这个二维数组有多少个一维数组。或者说二维表有几行

n:表示每一个一维数组的元素有多少个。或者说每一行共有几列

java 复制代码
// 创建一个3行4列的二维数组
int[][] arr = new int[3][4]; 
  • 不规则数组 (每一行列数可以不同)
java 复制代码
//(1)先确定总行数
// 此时只是确定了总行数,每一行里面现在是null
数据类型[][] 二维数组名 = new 数据类型[总行数][]; 

//(2)再确定每一行的列数,创建每一行的一维数组
// 此时已经new完的行的元素就有默认值了,没有new的行还是null
二维数组名[行下标] = new 数据类型[该行的总列数]; 

//(3)再为元素赋值
二维数组名[行下标][列下标] = 值;
java 复制代码
// 只指定行数
int[][] arr = new int[3][]; 

// 再为每一行(每个一维数组)单独开辟空间
arr[0] = new int[2]; // 第0行有2列
arr[1] = new int[3]; // 第1行有3列
arr[2] = new int[1]; // 第2行有1列

3、访问与遍历

(1)元素访问

  • 二维数组长度/行数二维数组名.length

  • **二维数组行下标的范围:**​[0, 二维数组名.length-1]​ (此时把二维数组看成一维数组的话,元素是行对象。)

  • **二维数组某一个元素:**​二维数组名[行下标] [列下标]​(即先确定行/组,再确定列。)

  • **二维数组某一行的列数:**​二维数组名[行下标].length​(二维数组的每一行是一个一维数组。)

(2)遍历方式:嵌套循环

java 复制代码
int[][] arr = {{1, 2}, {3, 4, 5}};
System.out.println("---- 标准for循环遍历 ----");
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(); // 换行
}
System.out.println("---- 增强for循环遍历 ----");
for (int[] row : arr) { // 遍历每一行(得到一个一维数组)
    for (int element : row) { // 遍历当前行中的每个元素
        System.out.print(element + "\t");
    }
    System.out.println(); // 换行
}

// 注意:上述代码中,二维数组的索引从0开始,到数组长度-1结束。
// 另外,二维数组的遍历方式与一维数组类似,只是多了一个外层循环。

相关推荐
程序员的世界你不懂34 分钟前
基于Java+Maven+Testng+Selenium+Log4j+Allure+Jenkins搭建一个WebUI自动化框架(4)集成Allure报表
java·selenium·maven
isNotNullX1 小时前
数据中台架构解析:湖仓一体的实战设计
java·大数据·数据库·架构·spark
皮皮林5511 小时前
“RPC好,还是RESTful好?”,这个问题不简单
java
Xiaouuuuua1 小时前
一个简单的脚本,让pdf开启夜间模式
java·前端·pdf
车车不吃香菇2 小时前
java idea 本地debug linux服务
java·linux·intellij-idea
浩瀚星辰20242 小时前
图论基础算法:DFS、BFS、并查集与拓扑排序的Java实现
java·算法·深度优先·图论
LjQ20403 小时前
Java的一课一得
java·开发语言·后端·web
苦学编程的谢3 小时前
SpringBoot项目的创建
java·spring boot·intellij-idea
武昌库里写JAVA3 小时前
vue3面试题(个人笔记)
java·vue.js·spring boot·学习·课程设计
别来无恙1494 小时前
整合Spring、Spring MVC与MyBatis:构建高效Java Web应用
java·spring·mvc