# 第 05 章_数组

讲师:尚硅谷 - 宋红康(江湖人称:康师傅)

官网:http://www.atguigu.com


# 本章专题与脉络

第1阶段:Java基本语法-第05章

# 1. 数组的概述

# 1.1 为什么需要数组

需求分析 1:

需要统计某公司 50 个员工的工资情况,例如计算平均工资、找到最高工资等。用之前知识,首先需要声明 50个变量 来分别记录每位员工的工资,这样会很麻烦。因此我们可以将所有的数据全部存储到一个容器中统一管理,并使用容器进行计算。

需求分析 2:

snipaste_20220317_000101

容器的概念:

  • ** 生活中的容器:** 水杯(装水等液体),衣柜(装衣服等物品),集装箱(装货物等)。
  • ** 程序中的容器:** 将多个数据存储到一起,每个数据称为该容器的元素。

# 1.2 数组的概念

  • 数组 (Array),是多个相同类型数据按一定顺序排列的集合,并使用一个名字命名,并通过编号的方式对这些数据进行统一管理。

  • 数组中的概念

    • 数组名
    • 下标(或索引)
    • 元素
    • 数组的长度

image-20220317000952499

数组的特点:

  • 数组本身是 引用数据类型 ,而数组中的元素可以是 任何数据类型 ,包括基本数据类型和引用数据类型。
  • 创建数组对象会在内存中开辟一整块 连续的空间 。占据的空间的大小,取决于数组的长度和数组中元素的类型。
  • 数组中的元素在内存中是依次紧密排列的,有序的。
  • 数组,一旦初始化完成,其长度就是确定不变的。数组的 长度一旦确定,就不能修改
  • 我们可以直接通过下标 (或索引) 的方式调用指定位置的元素,速度很快。
  • 数组名中引用的是这块连续空间的首地址

# 1.3 数组的分类

1、按照元素类型分:

  • 基本数据类型元素的数组:每个元素位置存储基本数据类型的值
  • 引用数据类型元素的数组:每个元素位置存储对象(本质是存储对象的首地址)(在面向对象部分讲解)

2、按照维度分:

  • 一维数组:存储一组数据
  • 二维数组:存储多组数据,相当于二维表,一行代表一组数据,只是这里的二维表每一行长度不要求一样

image-20211221164709624

# 2. 一维数组的使用

# 2.1 一维数组的声明

格式:

// 推荐
元素的数据类型[] 一维数组的名称;
// 不推荐
元素的数据类型  一维数组名[];

举例:

int[] arr;
int arr1[];
double[] arr2;
String[] arr3;  // 引用类型变量数组

数组的声明,需要明确:

(1)数组的维度:在 Java 中数组的符号是 [],[] 表示一维,[][] 表示二维。

(2)数组的元素类型:即创建的数组容器可以存储什么数据类型的数据。元素的类型可以是任意的 Java 的数据类型。例如:int、String、Student 等。

(3)数组名:就是代表某个数组的标识符,数组名其实也是变量名,按照变量的命名规范来命名。数组名是个引用数据类型的变量,因为它代表一组数据

举例:

public class ArrayTest1 {
    public static void main(String[] args) {
        // 比如,要存储一个小组的成绩
        int[] scores;
        int grades[];
//        System.out.println (scores);// 未初始化不能使用
        // 比如,要存储一组字母
        char[] letters;
        // 比如,要存储一组姓名
        String[] names;
        // 比如,要存储一组价格
        double[] prices;
    }
}

注意:Java 语言中 **声明数组时不能指定其长度**(数组中元素的个数)。 例如: int a [5]; // 非法

# 2.2 一维数组的初始化

# 2.2.1 静态初始化

  • 如果数组变量的初始化和数组元素的赋值操作同时进行,那就称为静态初始化。

  • 静态初始化,本质是用静态数据(编译时已知)为数组初始化。此时数组的长度由静态数据的个数决定

  • 一维数组声明和静态初始化格式 1:

    数据类型[] 数组名 = new 数据类型[]{元素1,元素2,元素3,...};
        
    数据类型[] 数组名;
    数组名 = new 数据类型[]{元素1,元素2,元素3,...};
    • new:关键字,创建数组使用的关键字。因为数组本身是引用数据类型,所以要用 new 创建数组实体。

例如,定义存储 1,2,3,4,5 整数的数组容器。

int[] arr = new int[]{1,2,3,4,5};// 正确
// 或
int[] arr;
arr = new int[]{1,2,3,4,5};// 正确
  • 一维数组声明和静态初始化格式 2
数据类型[] 数组名 = {元素1,元素2,元素3...};// 必须在一个语句中完成,不能分成两个语句写

例如,定义存储 1,2,3,4,5 整数的数组容器

int[] arr = {1,2,3,4,5};// 正确
int[] arr;
arr = {1,2,3,4,5};// 错误

举例:

public class ArrayTest2 {
    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5};// 右边不需要写 new int []
        int[] nums;
        nums = new int[]{10,20,30,40}; // 声明和初始化在两个语句完成,就不能省略 new int []
        char[] word = {'h','e','l','l','o'};
        String[] heros = {"袁隆平","邓稼先","钱学森"};
        System.out.println("arr数组:" + arr);//arr 数组:[I@1b6d3586
        System.out.println("nums数组:" + nums);//nums 数组:[I@4554617c
        System.out.println("word数组:" + word);//word 数组:[C@74a14482
        System.out.println("heros数组:" + heros);//heros 数组:[Ljava.lang.String;@1540e19d
    }
}

# 2.2.2 动态初始化

数组变量的初始化和数组元素的赋值操作分开进行,即为动态初始化。

动态初始化中,只确定了元素的个数(即数组的长度),而元素值此时只是默认值,还并未真正赋自己期望的值。真正期望的数据需要后续单独一个一个赋值。

格式:

数组存储的元素的数据类型[] 数组名字 = new 数组存储的元素的数据类型[长度];
数组存储的数据类型[] 数组名字;
数组名字 = new 数组存储的数据类型[长度];
  • [长度]:数组的长度,表示数组容器中可以最多存储多少个元素。

  • **注意:数组有定长特性,长度一旦指定,不可更改。** 和水杯道理相同,买了一个 2 升的水杯,总容量就是 2 升是固定的。

举例 1:正确写法

int[] arr = new int[5];
int[] arr;
arr = new int[5];

举例 2:错误写法

int[] arr = new int[5]{1,2,3,4,5};// 错误的,后面有 {} 指定元素列表,就不需要在 [] 中指定元素个数了。

# 2.3 一维数组的使用

# 2.3.1 数组的长度

  • 数组的元素总个数,即数组的长度
  • 每个数组都有一个属性 length指明它的长度,例如:arr.length 指明数组 arr 的长度 (即元素个数)
  • 每个数组都具有长度,而且一旦初始化,其长度就是确定,且是不可变的。

# 2.3.2 数组元素的引用

如何表示数组中的一个元素?

每一个存储到数组的元素,都会自动的拥有一个编号,从 0 开始,这个自动编号称为 数组索引(index)或下标 ,可以通过数组的索引 / 下标访问到数组中的元素。

数组名[索引/下标]

数组的下标范围?

Java 中数组的下标从 [0] 开始,下标范围是 [0, 数组的长度 - 1],即 [0, 数组名.length-1]

数组元素下标可以是整型常量 / 整型表达式。如 a [3] , b [i] , c [6*i];

举例

public class ArrayTest3 {
    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5};
        System.out.println("arr数组的长度:" + arr.length);
        System.out.println("arr数组的第1个元素:" + arr[0]);// 下标从 0 开始
        System.out.println("arr数组的第2个元素:" + arr[1]);
        System.out.println("arr数组的第3个元素:" + arr[2]);
        System.out.println("arr数组的第4个元素:" + arr[3]);
        System.out.println("arr数组的第5个元素:" + arr[4]);
        // 修改第 1 个元素的值
        // 此处 arr [0] 相当于一个 int 类型的变量
        arr[0] = 100;
        System.out.println("arr数组的第1个元素:" + arr[0]);
    }
}

# 2.4 一维数组的遍历

将数组中的每个元素分别获取出来,就是 遍历for 循环与数组的遍历是绝配。

举例 1

public class ArrayTest4 {
    public static void main(String[] args) {
        int[] arr = new int[]{1,2,3,4,5};
        // 打印数组的属性,输出结果是 5
        System.out.println("数组的长度:" + arr.length);
        // 遍历输出数组中的元素
        System.out.println("数组的元素有:");
        for(int i=0; i<arr.length; i++){
            System.out.println(arr[i]);
        }
    }
}

举例 2

public class ArrayTest5 {
    public static void main(String[] args) {
        int[] arr = new int[5];
        System.out.println("arr数组的长度:" + arr.length);
        System.out.print("存储数据到arr数组之前:[");
        for (int i = 0; i < arr.length; i++) {
            if(i==0){
                System.out.print(arr[i]);
            }else{
                System.out.print("," + arr[i]);
            }
        }
        System.out.println("]");
        // 初始化
 		/* 
 		arr[0] = 2;
        arr[1] = 4;
        arr[2] = 6;
        arr[3] = 8;
        arr[4] = 10;
        */
        for (int i = 0; i < arr.length; i++) {
            arr[i] = (i+1) * 2;
        }
        System.out.print("存储数据到arr数组之后:[");
        for (int i = 0; i < arr.length; i++) {
            if(i==0){
                System.out.print(arr[i]);
            }else{
                System.out.print("," + arr[i]);
            }
        }
        System.out.println("]");
    }
}

# 2.5 数组元素的默认值

数组是引用类型,当我们使用动态初始化方式创建数组时,元素值只是默认值。例如:

public class ArrayTest6 {
	public static void main(String argv[]){
		int a[]= new int[5]; 
		System.out.println(a[3]); //a [3] 的默认值为 0
	}
}
  • 基本数据类型:默认初始化值各有不同

  • 引用数据类型:默认初始化值为null(注意与 0 不同!)

public class ArrayTest7 {
    public static void main(String[] args) {
        // 存储 26 个字母
        char[] letters = new char[26];
        System.out.println("letters数组的长度:" + letters.length);
        System.out.print("存储字母到letters数组之前:[");
        for (int i = 0; i < letters.length; i++) {
            if(i==0){
                System.out.print(letters[i]);
            }else{
                System.out.print("," + letters[i]);
            }
        }
        System.out.println("]");
       // 存储 5 个姓名
        String[] names = new String[5];
        System.out.println("names数组的长度:" + names.length);
        System.out.print("存储姓名到names数组之前:[");
        for (int i = 0; i < names.length; i++) {
            if(i==0){
                System.out.print(names[i]);
            }else{
                System.out.print("," + names[i]);
            }
        }
        System.out.println("]");
    }
}

# 3. 一维数组内存分析

# 3.1 Java 虚拟机的内存划分

为了提高运算效率,就对空间进行了不同区域的划分,因为每一片区域都有特定的处理数据方式和内存管理方式。

JVM架构-简图

区域名称作用
虚拟机栈用于存储正在执行的每个 Java 方法的局部变量表等。局部变量表存放了编译期可知长度
的各种基本数据类型、对象引用,方法执行完,自动释放
堆内存存储对象(包括数组对象),new 创建的,都存储在堆内存。
方法区存储已被虚拟机加载的类信息、常量、(静态变量)、即时编译器编译后的代码等数据。
本地方法栈当程序中调用了native 的本地方法时,本地方法执行期间的内存区域
程序计数器程序计数器是 CPU 中的寄存器,它包含每一个线程下一条要执行的指令的地址

# 3.2 一维数组在内存中的存储

# 1、一个一维数组内存图

public static void main(String[] args) {
  	int[] arr = new int[3];
  	System.out.println(arr);//[I@5f150435
}

# 2、数组下标为什么是 0 开始

因为第一个元素距离数组首地址间隔 0 个单元格。

# 3、两个一维数组内存图

两个数组独立

public static void main(String[] args) {
    int[] arr = new int[3];
    int[] arr2 = new int[2];
    System.out.println(arr);
    System.out.println(arr2);
}

image-20231112013152051

# 4、两个变量指向一个一维数组

两个数组变量本质上代表同一个数组。

public static void main(String[] args) {
    // 定义数组,存储 3 个元素
    int[] arr = new int[3];
    // 数组索引进行赋值
    arr[0] = 5;
    arr[1] = 6;
    arr[2] = 7;
    // 输出 3 个索引上的元素值
    System.out.println(arr[0]);
    System.out.println(arr[1]);
    System.out.println(arr[2]);
    // 定义数组变量 arr2,将 arr 的地址赋值给 arr2
    int[] arr2 = arr;
    arr2[1] = 9;
    System.out.println(arr[1]);
}

image-20231112013206255

# 4. 一维数组的应用

** 案例 1:** 升景坊单间短期出租 4 个月,550 元 / 月(水电煤公摊,网费 35 元 / 月),空调、卫生间、厨房齐全。屋内均是 IT 行业人士,喜欢安静。所以要求来租者最好是同行或者刚毕业的年轻人,爱干净、安静。

public class ArrayTest {
      public static void main(String[] args) {
      int[] arr = new int[]{8,2,1,0,3};
      int[] index = new int[]{2,0,3,2,4,0,1,3,2,3,3};
      String tel = "";
      for(int i = 0;i < index.length;i++){
            tel += arr[index[i]];
      }
      System.out.println("联系方式:" + tel);
      }
}

案例 2:输出英文星期几

用一个数组,保存星期一到星期天的 7 个英语单词,从键盘输入 1-7,显示对应的单词

import java.util.Scanner;
/**
 * @author 尚硅谷 - 宋红康
 * @create 14:37
 */
public class WeekArrayTest {
    public static void main(String[] args) {
        //1. 声明并初始化星期的数组
        String[] weeks = {"Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"};
        //2. 使用 Scanner 从键盘获取 1-7 范围的整数
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入[1-7]范围的整数:");
        int number = scanner.nextInt();
        if(number < 1 || number > 7){
            System.out.println("你输入的输入非法");
        }else{
            //3. 根据输入的整数,到数组中相应的索引位置获取指定的元素(即:星期几)
            System.out.println("对应的星期为:" + weeks[number - 1]);
        }
        
        scanner.close();
    }
}

** 案例 3:** 从键盘读入学生成绩,找出最高分,并输出学生成绩等级。

  • 成绩 >= 最高分 - 10 等级为’A’

  • 成绩 >= 最高分 - 20 等级为’B’

  • 成绩 >= 最高分 - 30 等级为’C’

  • 其余 等级为’D’

提示:先读入学生人数,根据人数创建 int 数组,存放学生成绩。

image-20220317004637748
/**
 * @author 尚硅谷 - 宋红康
 * @create 14:55
 */
public class ScoreTest1 {
    public static void main(String[] args) {
        //1. 根据提示,获取学生人数
        System.out.print("请输入学生人数:");
        Scanner scanner = new Scanner(System.in);
        int count = scanner.nextInt();
        //2. 根据学生人数,创建指定长度的数组 (使用动态初始化)
        int[] scores = new int[count];
        //3. 使用循环,依次给数组的元素赋值
        int maxScore = 0; // 记录最高分
        System.out.println("请输入" + count + "个成绩");
        for (int i = 0; i < scores.length; i++) {
            scores[i] = scanner.nextInt();
            //4. 获取数组中元素的最大值,即为最高分
            if(maxScore < scores[i]){
                maxScore = scores[i];
            }
        }
        System.out.println("最高分是:" + maxScore);
        //5. 遍历数组元素,输出各自的分数,并根据其分数与最高分的差值,获取各自的等级
        char grade;
        for (int i = 0; i < scores.length; i++) {
            if(scores[i] >= maxScore - 10){
                grade = 'A';
            }else if(scores[i] >= maxScore - 20){
                grade = 'B';
            }else if(scores[i] >= maxScore - 30){
                grade = 'C';
            }else{
                grade = 'D';
            }
            System.out.println("student " + i + " socre is " + scores[i] + ", grade is " + grade);
        }
        // 关闭资源
        scanner.close();
    }
}

# 5. 多维数组的使用

# 5.1 概述

  • Java 语言里提供了支持多维数组的语法。

  • 如果说可以把一维数组当成几何中的 线性图形 ,那么二维数组就相当于是 一个表格 ,像 Excel 中的表格、围棋棋盘一样。

    image-20231112013219564

  • 应用举例 1:

    某公司 2022 年全年各个月份的销售额进行登记。按月份存储,可以使用一维数组。如下:

    int[] monthData = new int[]{23,43,22,34,55,65,44,67,45,78,67,66};

    如果改写为按 季度 为单位存储怎么办呢?

    int[][] quarterData = new int[][]<!--swig0-->;
    • 应用举例 2:

高一年级三个班级均由多个学生姓名构成一个个数组。如下:

String[] class1 = new String[]{"段誉","令狐冲","任我行"};
String[] class2 = new String[]{"张三丰","周芷若"};
String[] class3 = new String[]{"赵敏","张无忌","韦小宝","杨过"};

那从整个年级看,我们可以声明一个二维数组。如下:

String[][] grade = new String[][]<!--swig1-->;
  • 应用举例 3:

image-20220516095701345

蓝框的几个元素,可以使用一维数组来存储。但现在发现每个元素下还有下拉框,其内部还有元素,那就需要使用二维数组来存储:

image-20220516095829526

  • 使用说明

    1561524724397

  • 对于二维数组的理解,可以看成是一维数组 array1 又作为另一个一维数组 array2 的元素而存在。
  • 其实,从数组底层的运行机制来看,其实没有多维数组

# 5.2 声明与初始化

# 5.2.1 声明

二维数组声明的语法格式:

// 推荐
元素的数据类型[][] 二维数组的名称;
// 不推荐
元素的数据类型  二维数组名[][];
// 不推荐
元素的数据类型[]  二维数组名[];

例如:

public class Test20TwoDimensionalArrayDefine {
    public static void main(String[] args) {
        // 存储多组成绩
        int[][] grades;
        // 存储多组姓名
        String[][] names;
    }
}

面试

int[] x, y[];
//x是一维数组,y是二维数组

# 5.2.2 静态初始化

格式:

int[][] arr = new int[][]<!--swig2-->;

定义一个名称为 arr 的二维数组,二维数组中有三个一维数组

  • 每一个一维数组中具体元素也都已初始化
    • 第一个一维数组 arr [0] = {3,8,2};
    • 第二个一维数组 arr [1] = {2,7};
    • 第三个一维数组 arr [2] = {9,0,1,6};
  • 第三个一维数组的长度表示方式:arr [2].length;
  • 注意特殊写法情况:int [] x,y []; x 是一维数组,y 是二维数组。
  • 举例 1:
int[][] arr = <!--swig3-->;// 声明与初始化必须在一句完成
int[][] arr = new int[][]<!--swig4-->;
int[][] arr;
arr = new int[][]<!--swig5-->;
arr = new int[3][3]<!--swig6-->;// 错误,静态初始化右边 new 数据类型 [][] 中不能写数字
  • 举例 2:
public class TwoDimensionalArrayInitialize {
    public static void main(String[] args) {
        // 存储多组成绩
        int[][] grades = {
                    {89,75,99,100},
                    {88,96,78,63,100,86},
                    {56,63,58},
                    {99,66,77,88}
                };
        // 存储多组姓名
        String[][] names = {
            {"张三","李四", "王五", "赵六"},
            {"刘备","关羽","张飞","诸葛亮","赵云","马超"},
            {"曹丕","曹植","曹冲"},
            {"孙权","周瑜","鲁肃","黄盖"}
        };
    }
}

# 5.2.3 动态初始化

如果二维数组的每一个数据,甚至是每一行的列数,需要后期单独确定,那么就只能使用动态初始化方式了。动态初始化方式分为两种格式:

格式 1:规则二维表:每一行的列数是相同的

//(1)确定行数和列数
元素的数据类型[][] 二维数组名 = new 元素的数据类型[m][n];
	// 其中,m: 表示这个二维数组有多少个一维数组。或者说一共二维表有几行
	// 其中,n: 表示每一个一维数组的元素有多少个。或者说每一行共有一个单元格
// 此时创建完数组,行数、列数确定,而且元素也都有默认值
//(2)再为元素赋新值
二维数组名[行下标][列下标] =;

举例:

int[][] arr = new int[3][2];
  • 定义了名称为 arr 的二维数组

  • 二维数组中有 3 个一维数组

  • 每一个一维数组中有 2 个元素

  • 一维数组的名称分别为 arr [0], arr [1], arr [2]

  • 给第一个一维数组 1 脚标位赋值为 78 写法是: arr[0][1] = 78;

格式 2:不规则:每一行的列数不一样

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

举例:

int[][] arr = new int[3][];
  • 二维数组中有 3 个一维数组。
  • 每个一维数组都是默认初始化值 null (注意:区别于格式 1)
  • 可以对这个三个一维数组分别进行初始化:arr [0] = new int [3]; arr [1] = new int [1]; arr [2] = new int [2];
  • 注: int[][]arr = new int[][3]; // 非法

练习:

/*
 1
 2 2
 3 3 3
 4 4 4 4
 5 5 5 5 5
 */
public class Test25DifferentElementCount {
    public static void main(String[] args){
        //1、声明一个二维数组,并且确定行数
        // 因为每一行的列数不同,这里无法直接确定列数
        int[][]  arr = new int[5][];
        //2、确定每一行的列数
        for(int i=0; i<arr.length; i++){
			/*
			arr [0] 的列数是 1
			arr [1] 的列数是 2
			arr [2] 的列数是 3
			arr [3] 的列数是 4
			arr [4] 的列数是 5
			*/
            arr[i] = new int[i+1];
        }
        //3、确定元素的值
        for(int i=0; i<arr.length; i++){
            for(int j=0; j<arr[i].length; j++){
                arr[i][j] = i+1;
            }
        }
        //4、遍历显示
        for(int i=0; i<arr.length; i++){
            for(int j=0; j<arr[i].length; j++){
                System.out.print(arr[i][j] + " ");
            }
            System.out.println();
        }
    }
}

# 5.3 数组的长度和角标

  • 二维数组的长度 / 行数:二维数组名.length
  • 二维数组的某一行:二维数组名 [行下标],此时相当于获取其中一组数据。它本质上是一个一维数组。行下标的范围:[0, 二维数组名.length-1]。此时把二维数组看成一维数组的话,元素是行对象。
  • 某一行的列数:二维数组名 [行下标].length,因为二维数组的每一行是一个一维数组。
  • 某一个元素:二维数组名 [行下标][列下标],即先确定行 / 组,再确定列。
public class Test22TwoDimensionalArrayUse {
    public static void main(String[] args){
        // 存储 3 个小组的学员的成绩,分开存储,使用二维数组。
		/*
		int[][] scores1;
		int scores2[][];
		int[] scores3[];*/
        int[][] scores = {
                {85,96,85,75},
                {99,96,74,72,75},
                {52,42,56,75}
        };
        System.out.println(scores);//[[I@15db9742
        System.out.println("一共有" + scores.length +"组成绩.");
        //[[:代表二维数组,I 代表元素类型是 int
        System.out.println(scores[0]);//[I@6d06d69c
        //[:代表一维数组,I 代表元素类型是 int
        System.out.println(scores[1]);//[I@7852e922
        System.out.println(scores[2]);//[I@4e25154f
        //System.out.println(scores[3]);//ArrayIndexOutOfBoundsException: 3
        System.out.println("第1组有" + scores[0].length +"个学员.");
        System.out.println("第2组有" + scores[1].length +"个学员.");
        System.out.println("第3组有" + scores[2].length +"个学员.");
        System.out.println("第1组的每一个学员成绩如下:");
        // 第一行的元素
        System.out.println(scores[0][0]);//85
        System.out.println(scores[0][1]);//96
        System.out.println(scores[0][2]);//85
        System.out.println(scores[0][3]);//75
        //System.out.println(scores[0][4]);//java.lang.ArrayIndexOutOfBoundsException: 4
    }
}

# 5.4 二维数组的遍历

  • 格式:
for(int i=0; i<二维数组名.length; i++){ // 二维数组对象.length
    for(int j=0; j<二维数组名[i].length; j++){// 二维数组行对象.length
        System.out.print(二维数组名[i][j]);
    }
    System.out.println();
}
  • 举例:
public class Test23TwoDimensionalArrayIterate {
    public static void main(String[] args) {
        // 存储 3 个小组的学员的成绩,分开存储,使用二维数组。
        int[][] scores = {
                {85,96,85,75},
                {99,96,74,72,75},
                {52,42,56,75}
        };
        System.out.println("一共有" + scores.length +"组成绩.");
        for (int i = 0; i < scores.length; i++) {
            System.out.print("第" + (i+1) +"组有" + scores[i].length + "个学员,成绩如下:");
            for (int j = 0; j < scores[i].length; j++) {
                System.out.print(scores[i][j]+"\t");
            }
            System.out.println();
        }
    }
}

# 5.5 内存解析

二维数组本质上是元素类型是一维数组的一维数组

例子 1:

int[][] arr = {
    {1},
    {2,2},
    {3,3,3},
    {4,4,4,4},
    {5,5,5,5,5}
};

1562112672215

例子 2:

//1、声明二维数组,并确定行数和列数
int[][] arr = new int[4][5];
//2、确定元素的值
for (int i = 0; i < arr.length; i++) {
    for (int j = 0; j < arr.length; j++) {
        arr[i][j] = i + 1;
    }
}

1562113179785

例子 3:

//1、声明一个二维数组,并且确定行数
// 因为每一行的列数不同,这里无法直接确定列数
int[][]  arr = new int[5][];
//2、确定每一行的列数
for(int i=0; i<arr.length; i++){
    /*
			arr [0] 的列数是 1
			arr [1] 的列数是 2
			arr [2] 的列数是 3
			arr [3] 的列数是 4
			arr [4] 的列数是 5
			*/
    arr[i] = new int[i+1];
}
//3、确定元素的值
for(int i=0; i<arr.length; i++){
    for(int j=0; j<arr[i].length; j++){
        arr[i][j] = i+1;
    }
}

1562113981079

# 5.6 应用举例

** 案例 1:** 获取 arr 数组中所有元素的和。

提示:使用 for 的嵌套循环即可。

image-20220317005436209

** 案例 2:** 声明:int [] x,y []; 在给 x,y 变量赋值以后,以下选项允许通过编译的是:

数组之间的赋值,必须遵循的原则:

  • 维数一样
  • 数据类型一样
int[] arr1 = new int[3];
byte[] arr2 = new byte[4];
//arr1 = arr2; // 编译错误,int [] 和 byte [] 是不同的类型,不会进行自动类型提升!
System.out.println(arr1); // [I@6d311334
System.out.println(arr2); // [B@3d075dc0
int[][] arr3 = new int[4][];
//arr3 = arr1; // 编译错误,int [][] 和 int [] 的维数不一样
arr3[0] = arr1; // 编译通过,因为 int [] 和 int [] 的维数、类型相同
//arr3 [1] = arr2; // 编译错误,int [] 和 byte [] 是不同的类型,不会进行自动类型提升!
System.out.println(arr3); // [[I@448139f0
声明:int[] x,y[]; 在给x,y变量赋值以后,以下选项允许通过编译的是:
a)    x[0] = y;                 //no
b)    y[0] = x;                 //yes
c)    y[0][0] = x;              //no
d)    x[0][0] = y;              //no
e)    y[0][0] = x[0];           //yes
f)    x = y;                    //no
提示:
一维数组:int[] x  或者int x[]   
二维数组:int[][] y 或者  int[] y[]  或者 int  y[][]

** 案例 3:** 使用二维数组打印一个 10 行杨辉三角。

提示:

  1. 第一行有 1 个元素,第 n 行有 n 个元素

  2. 每一行的第一个元素和最后一个元素都是 1

  3. 从第三行开始,对于非第一个元素和最后一个元素的元素。即:

    yanghui[i][j] = yanghui[i-1][j-1] + yanghui[i-1][j];
    

image-20220317005549522

/**
 * @author 尚硅谷 - 宋红康
 * @create 10:11
 */
public class YangHuiTest {
    public static void main(String[] args) {
        //1. 动态初始化的方式创建二维数组
        int[][] yangHui = new int[10][];
        for (int i = 0; i < yangHui.length; i++) {
            yangHui[i] = new int[i + 1];
            //2. 给数组元素赋值
            // 2.1 给外层数组元素中的首元素和末元素赋值
            yangHui[i][0] = yangHui[i][i] = 1;
            //2.2 给外层数组元素中的非首元素和非末元素赋值(难)
            //if (i> 1){ // 从 i == 2 开始执行
                for(int j = 1;j < yangHui[i].length - 1;j++){ // 非首元素和非末元素的角标范围
                    yangHui[i][j] = yangHui[i-1][j-1] + yangHui[i-1][j];
                }
            //}
        }
        //3. 遍历二维数组
        for (int i = 0; i < yangHui.length; i++) {
            for (int j = 0; j < yangHui[i].length; j++) {
                System.out.print(yangHui[i][j] + "\t");
            }
            System.out.println();
        }
    }
}

# 6. 数组的常见算法

# 6.1 数值型数组特征值统计

  • 这里的特征值涉及到:平均值、最大值、最小值、总和等

** 举例 1:** 数组统计:求总和、均值

public class TestArrayElementSum {
    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];
        }
        double avg = (double)sum/arr.length;
        System.out.println("sum = " + sum);
        System.out.println("avg = " + avg);
    }
}

** 举例 2:** 求数组元素的总乘积

public class TestArrayElementMul {
    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 = " + result);
    }
}

** 举例 3:** 求数组元素中偶数的个数

public class TestArrayElementEvenCount {
    public static void main(String[] args) {
        int[] arr = {4,5,6,1,9};
        // 统计偶数个数
        int evenCount = 0;
        for(int i=0; i<arr.length; i++){
            if(arr[i]%2==0){
                evenCount++;
            }
        }
        System.out.println("evenCount = " + evenCount);
    }
}

** 举例 4:** 求数组元素的最大值

1574577970893

public class TestArrayMax {
    public static void main(String[] args) {
        int[] arr = {4,5,6,1,9};
        // 找最大值
        int 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 = " + max);
    }
}

** 举例 5:** 找最值及其第一次出现的下标

public class TestMaxIndex {
    public static void main(String[] args) {
        int[] arr = {4,5,6,1,9};
        // 找最大值以及第一个最大值下标
        int max = arr[0];
        int index = 0;
        for(int i=1; i<arr.length; i++){
            if(arr[i] > max){
                max = arr[i];
                index = i;
            }
        }
        System.out.println("max = " + max);
        System.out.println("index = " + index);
    }
}

** 举例 6:** 找最值及其所有最值的下标

public class Test13AllMaxIndex {
    public static void main(String[] args) {
        int[] arr = {4,5,6,1,9,9,3};
        // 找最大值
        int max = arr[0];
        for(int i=1; i<arr.length; i++){
            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");
            }
        }
        System.out.println();
    }
}

优化

public class Test13AllMaxIndex2 {
    public static void main(String[] args) {
        int[] arr = {4,5,6,1,9,9,3};
        // 找最大值
        int max = arr[0];
        String index = "0";
        for(int i=1; i<arr.length; i++){
            if(arr[i] > max){
                max = arr[i];
                index = i + "";
            }else if(arr[i] == max){
                index += "," + i;
            }
        }
        System.out.println("最大值是" + max);
        System.out.println("最大值的下标是[" + index+"]");
    }
}

** 举例 7 (难):** 输入一个整形数组,数组里有正数也有负数。数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和。求所有子数组的和的最大值。要求时间复杂度为 O (n)。
例如:输入的数组为 1, -2, 3, -10, -4, 7, 2, -5,和最大的子数组为 3, 10, -4, 7, 2,因此输出为该子数组的和 18。

public class Test5 {
	public static void main(String[] args) {
		int[] arr = new int[]{1, -2, 3, 10, -4, 7, 2, -5};
		int i = getGreatestSum(arr);
		System.out.println(i);
	}
	
	public static int getGreatestSum(int[] arr){
		int greatestSum = 0;
		if(arr == null || arr.length == 0){
			return 0;
		}
		int temp = greatestSum;
		for(int i = 0;i < arr.length;i++){
			temp += arr[i];
			
			if(temp < 0){
				temp = 0;
			}
			
			if(temp > greatestSum){
				greatestSum = temp;
			}
		}
		if(greatestSum == 0){
			greatestSum = arr[0];
			for(int i = 1;i < arr.length;i++){
				if(greatestSum < arr[i]){
					greatestSum = arr[i];
				}
			}
		}
		return greatestSum;
	}
}

举例 8:评委打分

分析以下需求,并用代码实现:

(1)在编程竞赛中,有 10 位评委为参赛的选手打分,分数分别为:5,4,6,8,9,0,1,2,7,3

(2)求选手的最后得分(去掉一个最高分和一个最低分后其余 8 位评委打分的平均值)

/**
 * @author 尚硅谷 - 宋红康
 * @create 10:03
 */
public class ArrayExer {
    public static void main(String[] args) {
        int[] scores = {5,4,6,8,9,0,1,2,7,3};
        int max = scores[0];
        int min = scores[0];
        int sum = 0;
        for(int i = 0;i < scores.length;i++){
            if(max < scores[i]){
                max = scores[i];
            }
            if(min > scores[i]){
                min = scores[i];
            }
            sum += scores[i];
        }
        double avg = (double)(sum - max - min) / (scores.length - 2);
        System.out.println("选手去掉最高分和最低分之后的平均分为:" + avg);
    }
}

# 6.2 数组元素的赋值与数组复制

** 举例 1:** 杨辉三角(见二维数组课后案例)

public class excise2 {
    public static void main(String[] args) {
        /*
        数组元素的赋值、数组复制:
            案例:使用二维数组打印一个 10 行杨辉三角。
            提示:
            1.第一行有 1 个元素,第 n 行有 n 个元素
            2.每一行的第一个元素和最后一个元素都是 1
            3.从第三行开始,对于非第一个元素和最后一个元素的元素。即:
                yanghui [i][j] = yanghui [i-1][j-1] + yanghui [i-1][j];
        */
        // 声明、初始化二维数组
        int[][] yanghui = new int[10][];
        // 给数组元素赋值
        for (int i = 0; i < yanghui.length; i++) {
            yanghui[i] = new int[i + 1]; // 每行的元素个数为行号 + 1
            // 给首末元素赋值
            yanghui[i][0] = yanghui[i][i] = 1;
            // 给每行的非首末元素赋值
            if (i > 1) { // 从第三行开始
                for (int j = 1; j < yanghui[i].length - 1; j++) {
                    yanghui[i][j] = yanghui[i - 1][j - 1] + yanghui[i - 1][j];
                }
            }
        }
        // 遍历二维数组
        for (int i = 0; i < yanghui.length; i++) {
            for (int j = 0; j < yanghui[i].length; j++) {
                System.out.print(yanghui[i][j] + "\t");
            }
            System.out.println();
        }
    }
}

** 举例 2:** 使用简单数组

(1) 创建一个名为 ArrayTest 的类,在 main () 方法中声明 array1 和 array2 两个变量,他们是 int [] 类型的数组。

(2) 使用大括号 {},把 array1 初始化为 8 个素数:2,3,5,7,11,13,17,19。

(3) 显示 array1 的内容。

(4) 赋值 array2 变量等于 array1,修改 array2 中的偶索引元素,使其等于索引值 (如 array [0]=0,array [2]=2)。打印出 array1。 array2 = array1;

** 思考:**array1 和 array2 是什么关系?

答:是两个变量,共同指向了堆空间中的同一个数组结构,即二者的地址值相同。

** 拓展:** 修改题目,实现 array2 对 array1 数组的复制

array2 = new int[array1.length];
for (int i = 0; i < array2.length; i++) {
    array2[i] = array1[i];
}

snipaste_20220317_225346snipaste_20220317_225359

** 举例 3:** 一个数组,让数组的每个元素去除第一个元素,得到的商作为被除数所在位置的新值。

public class Test3 {
	public static void main(String[] args) {
		int[] arr = new int[]{12,43,65,3,-8,64,2};
		
//		for(int i = 0;i < arr.length;i++){
//			arr[i] = arr[i] / arr[0];
//		}
		for(int i = arr.length -1;i >= 0;i--){
			arr[i] = arr[i] / arr[0];
		}
		// 遍历 arr
		for(int i = 0;i < arr.length;i++){
			System.out.print(arr[i] + " ");
		}
	}
}

** 举例 4:** 创建一个长度为 6 的 int 型数组,要求数组元素的值都在 1-30 之间,且是随机赋值。同时,要求元素的值各不相同。

public class Test4 {
	// 5-67 Math.random() * 63 + 5;
	@Test
	public void test1() {
		int[] arr = new int[6];
		for (int i = 0; i < arr.length; i++) {// [0,1) [0,30) [1,31)
			arr[i] = (int) (Math.random() * 30) + 1;
			boolean flag = false;
			while (true) {
				for (int j = 0; j < i; j++) {
					if (arr[i] == arr[j]) {
						flag = true;
						break;
					}
				}
				if (flag) {
					arr[i] = (int) (Math.random() * 30) + 1;
					flag = false;
					continue;
				}
				break;
			}
		}
		for (int i = 0; i < arr.length; i++) {
			System.out.println(arr[i]);
		}
	}
	// 更优的方法
public class excise3 {
    public static void main(String[] args) {
        // 举例 4∶创建一个长度为 6 的 int 型数组,要求数组元素的值都在 1-30 之间,且是随机赋值。同时,要求元素的值各不相同。
        // 声明、初始化数组
        int[] arr = new int[6];
        // 给数组元素赋值
        for (int i = 0; i < arr.length; i++) {
            // 随机生成 1-30 之间的数
            arr[i] = (int) (1 + Math.random() * (30 - 1 + 1));
            // 判断是否重复
            for (int j = 0; j < i; j++) {
                if (arr[i] == arr[j]) {
                    i--; // 重复则重新生成
                    break;
                }
            }
        }
        // 遍历数组
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
    }
}

** 举例 5:** 扑克牌

案例:遍历扑克牌

遍历扑克牌,效果如图所示:

image-20231112013234661

提示:使用两个字符串数组,分别保存花色和点数,再用一个字符串数组保存最后的扑克牌。
String [] hua = {"黑桃","红桃","梅花","方片"};
String[] dian = {"A","2","3","4","5","6","7","8","9","10","J","Q","K"};

package com.atguigu3.common_algorithm.exer5;
/**
 * @author 尚硅谷 - 宋红康
 * @create 17:16
 */
public class ArrayExer05 {
    public static void main(String[] args) {
        String[] hua = {"黑桃","红桃","梅花","方片"};
        String[] dian = {"A","2","3","4","5","6","7","8","9","10","J","Q","K"};
        String[] pai = new String[hua.length * dian.length];
        int k = 0;
        for(int i = 0;i < hua.length;i++){
            for(int j = 0;j < dian.length;j++){
                pai[k++] = hua[i] + dian[j];
            }
        }
        for (int i = 0; i < pai.length; i++) {
            System.out.print(pai[i] + "  ");
            if(i % 13 == 12){
                System.out.println();
            }
        }
    }
}

拓展:在上述基础上,增加大王、小王。

** 举例 6:** 回形数

从键盘输入一个整数(1~20) ,则以该数字为矩阵的大小,把 1,2,3…n*n 的数字按照顺时针螺旋的形式填入其中。

例如: 输入数字 2,则程序输出:
1 2
4 3

输入数字 3,则程序输出:
1 2 3
8 9 4
7 6 5
输入数字 4, 则程序输出:
1 2 3 4
12 13 14 5
11 16 15 6
10 9 8 7

// 方式 1
public class RectangleTest {
	public static void main(String[] args) {
		Scanner scanner = new Scanner(System.in);
		System.out.println("输入一个数字");
		int len = scanner.nextInt();
		int[][] arr = new int[len][len];
		
		int s = len * len;
		/*
		 * k = 1: 向右
		 * k = 2: 向下
		 * k = 3: 向左
		 * k = 4: 向上
		 */
		int k = 1;
		int i = 0,j = 0;
		for(int m = 1;m <= s;m++){
			if(k == 1){
				if(j < len && arr[i][j] == 0){
					arr[i][j++] = m;
				}else{
					k = 2;
					i++;  
					j--;
					m--;
				}
			}else if(k == 2){
				if(i < len && arr[i][j] == 0){
					arr[i++][j] = m;
				}else{
					k = 3;
					i--;
					j--;
					m--;
				}
			}else if(k == 3){
				if(j >= 0 && arr[i][j] == 0){
					arr[i][j--] = m;
				}else{
					k = 4;
					i--;
					j++;
					m--;
				}
			}else if(k == 4){
				if(i >= 0 && arr[i][j] == 0){
					arr[i--][j] = m;
				}else{
					k = 1;
					i++;
					j++;
					m--;
				}
			}
		}
		
		// 遍历
		for(int m = 0;m < arr.length;m++){
			for(int n = 0;n < arr[m].length;n++){
				System.out.print(arr[m][n] + "\t");
			}
			System.out.println();
		}
	}
}
// 方式 2
/*
	01 02 03 04 05 06 07 
	24 25 26 27 28 29 08 
	23 40 41 42 43 30 09 
	22 39 48 49 44 31 10 
	21 38 47 46 45 32 11 
	20 37 36 35 34 33 12 
	19 18 17 16 15 14 13 
 */
public class excise4 {
    public static void main(String[] args) {
        /* 回形数问题:
         * 从键盘输入一个整数(1~20) ,则以该数字为矩阵的大小,把 1,2,3…n*n 的数字按照顺时针螺旋的形式填入其中。*/
        // 1. 从键盘输入一个整数(1~20)
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入一个整数(1~20):");
        int n = sc.nextInt();
        // 2. 创建二维数组
        int[][] arr = new int[n][n];
        // 3. 定义变量
        int count = 1; // 计数器
        int minRow = 0; // 最小行
        int maxRow = n - 1; // 最大行
        int minCol = 0; // 最小列
        int maxCol = n - 1; // 最大列
        // 4. 循环赋值
        while (count <= n * n) {
            // 从左到右
            for (int x = minCol; x <= maxCol; x++) {
                arr[minRow][x] = count++;
            }
            minRow++;
            // 从上到下
            for (int y = minRow; y <= maxRow; y++) {
                arr[y][maxCol] = count++;
            }
            maxCol--;
            // 从右到左
            for (int x = maxCol; x >= minCol; x--) {
                arr[maxRow][x] = count++;
            }
            maxRow--;
            // 从下到上
            for (int y = maxRow; y >= minRow; y--) {
                arr[y][minCol] = count++;
            }
            minCol++;
        }
        // 5. 遍历二维数组
        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();
        }
    }
}

# 6.3 数组元素的反转

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

(头尾)双指针

image-20221117195931777
public class excise6_array_reverse {
    /*
    反转一个一维数组
    */
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        // 方法 1: 遍历一半,交换首尾对应位置
        for (int i = 0; i < arr.length / 2; i++) { // 注意这里的边界条件
            int tmp = arr[i];
            arr[i] = arr[arr.length - 1 - i]; // 首位对应位置互换
            arr[arr.length - 1 - i] = tmp;
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        // 方法 2:双指针
        int left = 0;
        int right = arr.length - 1;
        for (; left < right; left++, right--) {
            int tmp = arr[left];
            arr[left] = arr[right];
            arr[right] = tmp;
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
    }
}

1561469087319

public class TestArrayReverse2 {
    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5};
        System.out.println("反转之前:");
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
        // 反转
        // 左右对称位置交换
        for(int left=0,right=arr.length-1; left<right; left++,right--){
            // 首  与  尾交换
            int temp = arr[left];
            arr[left] = arr[right];
            arr[right] = temp;
        }
        System.out.println("反转之后:");
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}

# 6.4 数组的扩容与缩容

数组的扩容

题目:现有数组 int [] arr = new int; ,现将数组长度扩容 1 倍,并将 10,20,30 三个数据添加到 arr 数组中,如何操作?

public class excise7_array_expansion {
    public static void main(String[] args) {
        /* 数组的扩容:
            现有数组 int [] arr = new int []{1,2,3,4,5}; ,现将数组长度扩容 1 倍,
            并将 10,20,30 三个数据添加到 arr 数组中,如何操作? */
        int[] arr = {1, 2, 3, 4, 5};
        // 新建一个数组,长度为原数组的 2 倍。因为数组一旦初始化,长度就不可变了。
        int[] newArr = new int[arr.length * 2];
        // 将原数组中的数据复制到新数组中
        for (int i = 0; i < arr.length; i++) {
            newArr[i] = arr[i];
        }
        // 将 10,20,30 三个数据添加到 newArr 数组中
        newArr[arr.length] = 10;
        newArr[arr.length + 1] = 20;
        newArr[arr.length + 2] = 30;
        // 将新数组赋值给原数组
        arr = newArr;
        // 遍历数组
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}

数组的缩容

题目:现有数组 int [] arr={1,2,3,4,5,6,7}。现需删除数组中索引为 4 的元素。

public class excise8_array_shrinkage {
    public static void main(String[] args) {
        /* 数组的缩容:现有数组 int [] arr = new int []{1,2,3,4,5};,先需要删除索引为 3 的元素。*/
        int[] arr = {1, 2, 3, 4, 5};
        int delIndex = 3;
        // 方法 1:新建一个数组,长度为原数组的长度 - 1
        int[] newArr = new int[arr.length - 1];
        for (int i = 0; i < arr.length; i++) {
            if (i < delIndex) {
                newArr[i] = arr[i];
            } else if (i > delIndex) {
                newArr[i - 1] = arr[i];
            }
        }
        arr = newArr;
        // 遍历数组
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
        // 方法 2:直接在原数组上操作,将索引 3 后面的元素向前移动一位,然后将最后一位赋值为 0
        for (int i = delIndex; i < arr.length - 1; i++) { // 为了避免数组越界,这里的循环结束条件是 arr.length-1
            arr[i] = arr[i + 1];
        }
        arr[arr.length - 1] = 0;
        // 遍历数组
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}

# 6.5 数组的元素查找

1、顺序查找

  • 优点:
    • 对数组的顺序没要求
    • 算法简单
  • 缺点:执行效率低,时间复杂度为 O (N)
public class excise1_linear_search {
    public static void main(String[] args) {
        /* 线性查找 */
        int[] arr = {1, 9, 11, -1, 34, 89};
        int target = -1; // 目标值
        int index = -1; // 目标值的下标
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] == target) {
                index = i;
                break; // 找到了就不再找了
            }
        }
        if (index == -1) {
            System.out.println("没有找到");
        } else {
            System.out.println("找到了,下标为:" + index);
        }
    }
}

2、二分查找

  • 优点:执行效率高,时间复杂度为 O (log2N)
  • 缺点:
    • 要求数组必须有序
    • 算法较顺序查找更复杂

举例:

image-20220317230955644

实现步骤:

image-20231112013247174

public class excise2_binary_search {
    public static void main(String[] args) {
        /* 二分查找 */
        int[] arr = new int[]{1, 3, 8, 10, 11, 67, 100}; // 必须有序
        int target = 3; // 目标值
        int left = 0;
        int right = arr.length - 1;
        int mid = (left + right) / 2;
        while (left <= right) {
            if (arr[mid] == target) {
                System.out.println("找到了,下标为:" + mid);
                break;
            } else if (arr[mid] > target) {
                right = mid - 1; // 向左查找
            } else {
                left = mid + 1; // 向右查找
            }
            mid = (left + right) / 2;
        }
        if (left > right) { // 说明退出 while 循环,没有找到
            System.out.println("没有找到");
        }
    }
}

# 6.6 数组元素排序

# 6.6.1 算法概述

  • 定义

    • 排序:假设含有 n 个记录的序列为 {R1,R2,...,Rn}, 其相应的关键字序列为 {K1,K2,...,Kn}。将这些记录重新排序为 {Ri1,Ri2,...,Rin}, 使得相应的关键字值满足条 Ki1<=Ki2<=...<=Kin, 这样的一种操作称为排序。
    • 通常来说,排序的目的是快速查找
  • 衡量排序算法的优劣:

    • 时间复杂度 :分析关键字的比较次数和记录的移动次数

    • 空间复杂度 :分析排序算法中需要多少辅助内存

      一个算法的空间复杂度S(n)定义为该算法所耗费的存储空间,它也是问题规模n的函数。
      
    • 稳定性 若两个记录 A 和 B 的关键字值相等,但排序后 A、B 的先后次序保持不变,则称这种排序算法是稳定的。

      排序算法的特有指标

      image-20211222113701365

# 6.6.2 排序算法概述

  • 排序算法分类:内部排序和外部排序

    • 内部排序 :整个排序过程不需要借助于外部存储器(如磁盘等),所有排序操作都在内存中完成
    • 外部排序 :参与排序的数据非常多,数据量非常大,计算机无法把整个排序过程放在内存中完成,必须借助于外部存储器(如磁盘)。外部排序最常见的是多路归并排序。可以认为外部排序是由多次内部排序组成
  • 十大内部排序算法

​ 数组的排序算法很多,实现方式各不相同,时间复杂度、空间复杂度、稳定性也各不相同:

image-20211222111142684

常见时间复杂度所消耗的时间从小到大排序:

O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)

注意,经常将以 2 为底 n 的对数简写成 logn。

image-20220824003440106

# 6.6.3 冒泡排序(Bubble Sort)

image-20220516094637228

排序思想:

  1. 比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。

  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这一步做完后,最后的元素会是最大的数

  3. 针对所有的元素重复以上的步骤,除了最后一个

  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较为止。

BubbleSort

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

/*
1、冒泡排序(最经典)
思想:每一次比较 “相邻(位置相邻)” 元素,如果它们不符合目标顺序(例如:从小到大),
     就交换它们,经过多轮比较,最终实现排序。
	 (例如:从小到大)	 每一轮可以把最大的沉底,或最小的冒顶。
*/
public class excise3_bubble_sort {
    public static void main(String[] args) {
        /* 冒泡排序:
        目标:从小到大
        冒泡排序的轮数 = 元素的总个数 - 1
        轮数是多轮,每一轮比较的次数是多次,需要用到双重循环,即循环嵌套
        外循环:控制轮数,
        内循环:控制每一轮的比较次数和过程 */
        int[] arr = new int[]{3, 9, -1, 10, 20, 5, 2, 42, -3, 0};
        for (int i = 0; i < arr.length - 1; i++) { // 一共要比较多少轮:arr.length - 1
            for (int j = 0; j < arr.length - 1 - i; j++) { // 每一轮要比较多少次:arr.length - 1 - i
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
    }
}

冒泡排序优化(选讲)

public class excise4_optimized_bubble_sort {
    public static void main(String[] args) {
        /* 优化冒泡排序:
         因为冒泡排序是嵌套两层循环,外层循环控制轮数,内层循环控制每一轮的比较次数和过程,
         可以考虑如果某一轮内层循环没有发生交换,说明数组已经有序,不需要再进行下一轮的比较了,
         可以直接退出外层循环,这样就可以减少比较的次数 */
        int[] arr = new int[]{3, 9, -1, 10, 20, 5, 2, 42, -3, 0};
        boolean isSorted = true; // 用于标记数组是否有序
        for (int i = 0; i < arr.length - 1; i++) {
            for (int j = 0; j < arr.length - 1 - i; j++) {
                if (arr[j] > arr[j + 1]) {
                    int tmp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = tmp;
                    isSorted = false; // 有发生交换,说明数组仍不是有序的,至少需要再比较一轮
                }
            }
            if (isSorted) {
                break; // 说明内层循环没有发生交换,说明数组已经有序,不需要再进行下一轮的比较了
            }
        }
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
    }
}

# 6.6.4 快速排序

**快速排序(Quick Sort)** 由 图灵奖 获得者 Tony Hoare 发明,被列为 20世纪十大算法之一 ,是迄今为止所有内部排序算法中速度最快的一种,快速排序的时间复杂度为O(nlog(n))O(nlog(n))

快速排序通常 **明显比同为O(nlogn)O(nlogn) 的其他算法更快**,因此常被采用,而且快排采用了分治法的思想,所以在很多笔试面试中能经常看到快排的影子。

排序思想:

  1. 从数列中挑出一个元素,称为基准(pivot)

  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为 **分区(partition)** 操作。

  3. **递归(recursive)** 地把小于基准值元素的子数列和大于基准值元素的子数列排序。

  4. 递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

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

图示 1:

image-20220317235922776

图示 2:

第一轮操作:

image-20221117205612230

第二轮操作:

image-20221117205719427

代码实现:

public class excise5_fast_sort {
    public static void main(String[] args) {
        /* 快速排序的思路:
        1. 从数列中挑出一个元素,称为 “基准”(pivot);
        2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。
        在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
        3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
        4. 递归的第一步,结束条件是数列的大小是零或一,此时该数列显然已经有序。 */
        int[] arr = new int[]{3, 9, -1, 10, 20, 5, 2, 42, -3, 0};
        quickSort(arr); // 调用快速排序方法
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
    }
    private static void quickSort(int[] arr) {
        subSort(arr, 0, arr.length - 1);
    }
    private static void subSort(int[] arr, int start, int end) {
        if (start < end) {
            int pivot = arr[start]; // 基准值
            int low = start;
            int high = end + 1; // 为了防止越界,所以先加 1
            while (true) {
                while (low < end && arr[++low] <= pivot) {
                    ; //low 从左往右找到第一个大于基准值的元素
                }  // 循环结束时,low 指向的元素大于基准值,或者 low 已经到了 end 的位置
                while (high > start && arr[--high] >= pivot) {
                    ; //high 从右往左找到第一个小于基准值的元素
                }  // 循环结束时,high 指向的元素小于基准值,或者 high 已经到了 start 的位置
                if (low < high) { // 如果 low < high,说明还没有遍历完
                    // 交换 low 和 high 的值
                    swap(arr, low, high);
                } else { // 如果 low >= high,说明已经遍历完了
                    break;
                }
            }
            // 交换基准值和 high 的值:为什么是 high?因为 high 是最后一个小于基准值的元素。这样就把基准值放到了中间
            swap(arr, start, high);
            // 递归调用:此时基准值在 high 的位置,因此以 high 为界限,分治
            subSort(arr, start, high - 1);
            subSort(arr, high + 1, end);
        }
    }
    private static void swap(int[] arr, int a, int b) {
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
}

# 6.6.5 内部排序性能比较与选择

  • 性能比较
    • 从平均时间而言快速排序最佳。但在最坏情况下时间性能不如堆排序归并排序
    • 从算法简单性看:由于直接选择排序、直接插入排序和冒泡排序的算法比较简单,将其认为是简单算法。对于 Shell 排序、堆排序、快速排序和归并排序算法,其算法比较复杂,认为是复杂排序
    • 从稳定性看直接插入排序、冒泡排序和归并排序是稳定的;而直接选择排序、快速排序、 Shell 排序和堆排序是不稳定排序
    • 从待排序的记录数 n 的大小看,n 较小时,宜采用简单排序;而 n 较大时宜采用改进排序。
  • 选择
    • 若 n 较小 (如 n≤50),可采用直接插入或直接选择排序。
      • 当记录规模较小时,直接插入排序较好;
      • 否则,因为直接选择移动的记录数少于直接插入,应选直接选择排序为宜。
    • 若 n 较大,则应采用时间复杂度为O(nlogn)O(nlogn) 的排序方法:快速排序堆排序归并排序
    • 若文件初始状态基本有序 (指正序),则应选用直接插入冒泡随机快速排序为宜;

# 7. Arrays 工具类的使用

**java.util.Arrays 类** 即为操作数组的工具类,包含了用来操作数组(比如排序和搜索)的各种方法。 比如:

  • 数组元素遍历打印输出
    • static String toString(int[] a) :字符串表示形式由数组的元素列表组成,括在方括号("[]")中。相邻元素用字符 ","(逗号加空格)分隔。形式为:[元素 1,元素 2,元素 3。。。]
    • static String toString(Object[] a) :字符串表示形式由数组的元素列表组成,括在方括号("[]")中。相邻元素用字符 ","(逗号加空格)分隔。元素将自动调用自己从 Object 继承的 toString 方法将对象转为字符串进行拼接,如果没有重写,则返回类型 @hash 值,如果重写则按重写返回的字符串进行拼接。
  • 数组排序
    • static void sort(int[] a) :将 a 数组按照从小到大进行排序
    • static void sort(int[] a, int fromIndex, int toIndex) :将 a 数组的 [fromIndex, toIndex) 部分按照升序排列
    • static void sort(Object[] a) :根据元素的自然顺序对指定对象数组按升序进行排序。
    • static <T> void sort(T[] a, Comparator<? super T> c) :根据指定比较器产生的顺序对指定对象数组进行排序。
  • 数组元素的二分查找
    • static int binarySearch(int[] a, int key)static int binarySearch(Object[] a, Object key)要求数组有序,在数组中查找 key 是否存在,如果存在返回第一次找到的下标,不存在返回负数。
  • 数组的复制
    • static int[] copyOf(int[] original, int newLength) :根据 original 原数组复制一个长度为 newLength的新数组,并返回新数组
    • static <T> T[] copyOf(T[] original,int newLength) :根据 original 原数组复制一个长度为 newLength 的新数组,并返回新数组
    • static int[] copyOfRange(int[] original, int from, int to) :复制 original 原数组的 [from,to) 构成新数组,并返回新数组
    • static <T> T[] copyOfRange(T[] original,int from,int to) :复制 original 原数组的 [from,to) 构成新数组,并返回新数组
  • 比较两个数组是否相等
    • static boolean equals(int[] a, int[] a2) :比较两个数组的长度、元素是否完全相同
    • static boolean equals(Object[] a,Object[] a2) :比较两个数组的长度、元素是否完全相同
  • 填充数组
    • static void fill(int[] a, int val) :用 val 值填充整个a 数组
    • static void fill(Object[] a,Object val) :用 val 对象填充整个 a 数组
    • static void fill(int[] a, int fromIndex, int toIndex, int val) :将 a 数组 [fromIndex,toIndex)部分填充为 val 值
    • static void fill(Object[] a, int fromIndex, int toIndex, Object val) :将 a 数组 [fromIndex,toIndex) 部分填充为 val 对象

举例:java.util.Arrays 类的 sort () 方法提供了数组元素排序功能:

import java.util.Arrays;
public class SortTest {
	public static void main(String[] args) {
		int[] arr = {3, 2, 5, 1, 6};
        System.out.println("排序前" + Arrays.toString(arr));
        Arrays.sort(arr);
        System.out.println("排序后" + Arrays.toString(arr));
	}
}

# 8. 数组中的常见异常

# 8.1 数组角标越界异常

当访问数组元素时,下标指定超出 [0, 数组名.length-1] 的范围时,就会报数组下标越界异常: ArrayIndexOutOfBoundsException

public class TestArrayIndexOutOfBoundsException {
    public static void main(String[] args) {
        int[] arr = {1,2,3};
       // System.out.println ("最后一个元素:" + arr [3]);// 错误,下标越界
      //  System.out.println ("最后一个元素:" + arr [arr.length]);// 错误,下标越界
        System.out.println("最后一个元素:" + arr[arr.length-1]);// 对
    }
}

创建数组,赋值 3 个元素,数组的索引就是 0,1,2,没有 3 索引,因此我们不能访问数组中不存在的索引,程序运行后,将会抛出 ArrayIndexOutOfBoundsException 数组越界异常。在开发中,数组的越界异常是不能出现的,一旦出现了,就必须要修改我们编写的代码。

# 8.2 空指针异常

观察一下代码,运行后会出现什么结果。

public class TestNullPointerException {
    public static void main(String[] args) {
        // 定义数组
        int[][] arr = new int[3][];
        System.out.println(arr[0][0]);//NullPointerException
    }
}

因为此时数组的每一行还未分配具体存储元素的空间,此时 arr [0] 是 null,此时访问 arr [0][0] 会抛出 NullPointerException 空指针异常。

空指针异常在内存图中的表现

1572338767825

小结:空指针异常情况

// 举例一:
//		int[] arr1 = new int[10];
//		arr1 = null;
//		System.out.println(arr1[9]);
		
		// 举例二:
//		int[][] arr2 = new int[5][];
//		//arr2[3] = new int[10];
//		System.out.println(arr2[3][3]);
		
		// 举例三:
		String[] arr3 = new String[10];
		System.out.println(arr3[2].toString());