JAVA编程讲义之数组
在程序开发过程中,有时候需要存储大量的同类型数据。例如,存储一个班级50名学生的姓名,这时需要定义50个变量来保存姓名数据,但这种做法太繁琐了。那么,如何解决这类问题呢?Java语言提供了数组结构,它类似于一个容器,可以批量存储相同数据类型的元素。因此,对于前述学生成绩统计问题,我们只需要定义一个长度为50的字符串数组就可以解决。本章将对数组的基本概念、定义方式、初始化以及使用等内容展开讲解。
4.1 一维数组
一维数组的逻辑结构是线性表,它是最简单的数组。使用一维数组时,要先定义,然后做初始化,最后才能使用。本节为大家详细讲解一维数组的具体用法。
4.1.1 一维数组的创建
在Java中使用数组,一般需要3个步骤:声明数组、创建数组对象空间、为数组元素赋值。其中,声明数组和创建数组对象空间的语法如下:
数据类型[] 数组名; // 声明一维数组,这种是推荐的写法
数据类型 数组名[]; // 声明一维数组的第二种写法
数组名 = new 数据类型[数组元素个数]; // 为数组对象分配内存空间
在上面一维数组的声明语句中,“数据类型”可以是Java语言中的任意数据类型,包括简单类型和对象类型。“数组名”是用来访问数组的标识,作用与变量名类似,在程序中可以通过“数组名”访问数组中的元素,“数组名”的命名规则与变量的命名规则相同。另外,“[ ]”是定义数组类型的符号,声明数组时必须要有这个符号,这个符号可以放在数据类型后面,也可以放在数组名后面。
数组声明之后,就需要为数组对象分配一个固定长度的存储空间。在为数组分配内存空间的时候,需要使用new运算符。通过new运算符可以告诉编译器声明的数组存储什么类型的数据,以及数组要存储的元素个数。数组一旦创建长度就固定了,不能再次改变。
针对数组的声明和内存分配,举例说明如下:
int[] nums; // 声明数组名为nums的整形数组
nums = new int[10]; // 为nums数组分配10个存储整数的内存空间
在这例子中,第1行代码只是声明了一个整数类型的数组变量nums,但是这个变量没有指向任何一个数组对象空间,声明的数组变量是在JVM的栈内存中分配空间。第2行代码先通过new运算符创建了一个长度为10的整型数组空间,创建的数组空间是在JVM的堆内存中分配的。接着把数组对象赋值给nums,也就是将数组对象的地址存储到了nums变量中。此时,nums变量就指了这个数据对象上,因此通过nums可以访问到数组对象空间中的每一个元素。对于数组声明和数组对象内存分配,可以参考图4.1。
(a)只声明数组变量但未指向数组对象空间 (b)声明数组并指向数组对象空间
图4.1 数组声明和内存分配
图4.1中,左图声明了一个整型数组变量nums,这个变量对应一个栈内存的空间。因为nums没有指向数组对象,所以nums存储的内容是null。右图中在堆内存中创建了一个连续的长度为10的整型数组对象空间,数组对象的首地址是0x8A21。右图中声明的数组变量nums存储了数组对象的首地址。因此nums变量指向了这个数组对象,与这个数组对象有了引用关联。通过nums变量可以用下标索引的方式,访问数组中的每个元素。
另外,也可以使用一条语句完成数组的声明和内存分配,语法说明如下:
数据类型[] 数组名 = new 数据类型[数组元素个数]
使用这种方式会在定义数组的同时为数组分配内存空间,举例说明如下:
int[] nums = new int[10]; // 声明数组的同时为数组分配内存空间
在这行代码中,等号左边的nums是声明的数组变量,它指向了右边使用new运算符创建的数组对象。
知识点拨:在Java中,数组对象被创建之后,数组中的元素都具有一个初始的默认值。整型数组的元素默认值是0。浮点类型数组的元素默认值是0.0。布尔类型数组的元素默认值是false。字符串和对象等引用类型数组的元素默认值都是null。
4.1.2 数组元素的分配
在创建数组对象之后,就可以存储数据到数组元素空间中,进行数组元素的分配,也就是为数组元素赋值。为数组元素赋值的方式有3种:静态初始化、动态初始化、通过数组下标为数组赋值。
1.静态初始化
静态初始化就是在声明数组时,由开发者显式指定每个数组元素的初始值,初始值的类型要和定义数组的类型一致。根据这些初始值,系统会自动创建对应长度的数组空间,用于存储每个数组元素的数据。静态初始化语法如下:
数据类型[] 数组名 = {数据1,数据2,数据3,…,数据n}; // 静态初始化,第1种方式
数据类型[] 数组名;
数组名 = new 数据类型[]{数据1,数据2,数据3,…,数据n}; // 静态初始化,第2种方式
第1种静态初始化的方式,需要在声明数组的同时进行数据初始化,初始化的数据要写在大括号中,并以逗号分隔。第2种静态初始化的方式可以先声明数组变量,然后使用new运算符进行数组元素的初始化。另外,第2种方式中,右边表达式中的“[ ]”不允许写数组长度,否则会发生语法错误。
静态初始化示例代码如下:
int[] nums = {10,20,30,40,50}; // 声明数组并进行数组静态初始化
String[] names; // 声明数组变量
names = new String[]{"唐僧","孙悟空","猪八戒","沙和尚"}; // 进行静态初始化
2.动态初始化
进行数组动态初始化时,开发者只需要指定数组长度,然后由系统自动为数组元素分配初始值。动态初始化的语法格式如下:
数据类型[] 数组名 = new 数据类型[数组长度];
在进行动态初始化后,程序会根据指定的数组长度,创建对应长度的数组元素空间,并为每个数组元素空间设置初始值。
动态初始化的示例代码如下:
int[] nums = new int[5]; // 创建长度为5的整型数组,数组元素的初始值都是0
String[] names = new String[3]; // 创建长度为3的字符串数组,数组元素的初始值都为null
3.通过数组下标为数组赋值
在数组创建之后,可以使用数组名结合数组下标的方式,为数组空间中的每个元素赋值。使用数组下标赋值的语法格式如下:
数据类型[] 数组名 = new 数据类型[数组长度];
数组名[下标1] = 数值1;
数组名[下标2] = 数值2;
…
数组名[数组长度-1] = 数值n;
在通过数组下标为数组元素赋值的时候,数组下标的取值范围从0到数组长度减1为止。下标超出这个范围,会发生“ArrayIndexOutOfBoundsException”数组下标越界的异常。
通过数组下标为元素赋值的代码示例如下:
String[] names = new String[4]; // 声明长度为4的字符串数组
names[0] = "唐僧"; // 通过下标为每个数组元素赋值
names[1] = "孙悟空";
names[2] = "猪八戒";
names[3] = "沙和尚";
4.1.3 数组元素的访问
数组创建之后,最常用的操作就是访问数组元素,这包含为数组元素赋值和输出数组元素中的值。访问数组元素的方式是通过数组名结合数组下标的方式完成的。
接下来,通过实例来演示如何访问数组元素,如例4-1所示。
例4-1 Demo0401.java
1 package com.aaa.p040103;
2
3 public class Demo0401 {
4 public static void main(String[] args) {
5 int[] nums = new int[3]; // 定义长度为3的整型数组
6 nums[0] = 10; // 为数组元素赋值
7 nums[1] = 20;
8 nums[2] = 30;
9 System.out.println(nums[0]); // 输出数组元素中的值
10 System.out.println(nums[1]);
11 System.out.println(nums[2]);
12 }
13 }
程序运行结果如下:
10
20
30
在例4-1中,先创建了一个长度为3的整型数组,然后使用数组名结合下标的方式分别为3个数组元素赋值。注意,数组的下标必须写在中括号内。最后,使用打印输出语句输出每个数组元素内的数据。
4.1.4 length的使用
要获取数组的长度,可以通过数组对象的length属性得到。每个数组都会有length属性,当通过length属性获取了数组长度之后,就可以通过循环的方式,使用下标逐一遍历数组中的每个元素。
接下来,我们使用length获取数组长度,并通过循环遍历数组元素,如例4-2所示。
例4-2 Demo0402.java
1 package com.aaa.p040104;
2
3 public class Demo0402 {
4 public static void main(String[] args) {
5 int[] nums = new int[3]; // 创建长度为3的数组对象
6 nums[0] = 10; // 为数组元素赋值
7 nums[1] = 20;
8 nums[2] = 30;
9 for(int i = 0;i < nums.length;i++) { // 使用length获取数组长度,作为循环条件
10 System.out.println(nums[i]); // 循环输出每个元素
11 }
12 }
13 }
程序运行结果如下:
10
20
30
在例4-2中,先定义了长度为3的整型数组,然后使用数组名结合下标的方式为每个数组元素赋值,接着使用数组的length属性获取数组的长度,作为for循环的循环条件,最后通过for循环逐一遍历数组元素,并打印输出。
4.1.5 使用foreach遍历数组
除了使用for循环遍历数组外,Java中还有另外一种很简洁的遍历方法:foreach循环遍历。这种方式也称为增强for循环,它的功能比较强大,遍历时不需要依赖数组的长度和下标,即可实现数组遍历。foreach循环的语法格式如下:
for (数组中元素类型 临时变量 : 数组对象变量){
程序语句;
}
通过上面的语法结构可以看出,foreach遍历数组的时候不需要获取数组长度,也不需要用索引去访问数组中的元素,这是与for循环不同的地方。foreach循环会自动将数组中的元素逐一取出,存入一个临时变量中,然后使用临时变量进行数据处理,从而完成数组元素的遍历。
接下来,通过案例来演示foreach循环遍历数组,如例4-3所示。
例4-3 Demo0403.java
1 package com.aaa.p040105;
2
3 public class Demo0403 {
4 public static void main(String[] args) {
5 String[] names = {"唐僧","孙悟空","猪八戒","沙和尚"}; // 声明数组并进行初始化
6 for(String name : names) { // 使用foreach循环逐一取出数组元素并存入临时变量
7 System.out.println(name); // 输出临时变量存储的数据
8 }
9 }
10 }
程序运行结果如下:
唐僧
孙悟空
猪八戒
沙和尚
例4-3中,首先定义了一个数组对象,并初始化了4个字符串数据。然后使用foreach循环遍历数组,每次循环时foreach都通过临时变量存储当前遍历到的元素,并将元素打印显示。
注意:foreach循环代码简洁,编写方便,但是有其局限性,当使用foreach遍历数组时,只能访问其中的元素,不能对元素进行修改。
接下来,通过案例进一步演示在使用foreach循环的过程中,对元素进行修改会有什么结果,如例4-4所示。
例4-4 Demo0404.java
1 package com.aaa.p040105;
1
1 public class Demo0404 {
1 public static void main(String[] args) {
1 String[] strs = new String[3]; // 创建一个长度为3的数组
1 int i = 0;
1 for(String str : strs) { // 循环遍历数组
1 str = new String("字符串:" + i ); // 修改每个遍历到的值
1 i++;
1 }
1 for(String str : strs) {
1 System.out.println(str); // 打印数组中的值
1 }
1 }
1 }
程序运行结果如下:
null
null
null
例4-4中,先定义一个长度为3的字符串数组。然后通过第1个foreach循环,将遍历到的每个数组元素的数据都进行了修改。但在第2个foreach循环中,遍历输出的每个元素依旧是null。这说明在使用foreach循环遍历时,遍历的元素并没有真正被修改。原因是第8行中只是将临时变量str指向了一个新字符串,变量str和数组中的元素实际上没有任何联系。所以,foreach循环的过程中无法修改所遍历的数据。因此,foreach并不能替代for循环,仅仅是让遍历的方法变得更简洁。
4.1.6 基本类型数组的初始化
按照数据类型的不同,数组可分为基本类型数组和引用类型数组。基本类型数组的特点是,数组元素的值是直接存储在数组元素中的。所以,定义基本类型数组并初始化时,会先为数组分配空间,然后将数据直接存入对应的数组元素中。基本类型数组的初始化示例如图4.2所示。
(a)定义值类型数组并由系统自动初始化 (b)为值类型数组设置数据
图4.2 值类型数组的初始化和设值
在图4.2中,左图定义了一个值类型数组,也就是整型数组nums。该数组长度为5,初始化后每个数组元素的值都是0。右图是为nums数组中的元素都设置一个整数值。从图4.2中可以看出,值类型数组的数据都是直接存储在数组元素中的。
接下来,通过案例来演示值类型数组的初始化和设值,如例4-5所示。
例4-5 Demo0405.java
1 package com.aaa.p040106;
2
3 public class Demo0405 {
4 public static void main(String[] args) {
5 int[] nums = new int[5]; // 定义长度为5的数组,并动态初始化
6 for(int n : nums) { // 使用foreach输出每个元素值
7 System.out.println(n);
8 }
9
10 System.out.println("=========="); // 输出分隔符
11
12 nums[0] = 34; // 为每个数组元素设置特定值
13 nums[1] = 21;
14 nums[2] = 15;
15 nums[3] = 56;
16 nums[4] = 71;
17
18 for(int i = 0;i < nums.length;i++) { // 使用for循环输出每个元素
19 System.out.println(nums[i]);
20 }
21 }
22 }
程序运行结果如下:
0
0
0
0
0
==========
34
21
15
56
71
在例4-5中,先定义了一个长度为5的整型数组。接着,使用foreach循环输出数组元素中的数据,此时数组中的数据都为0。然后,通过数组名结合下标的方式,为每个数组元素设置特定数据,最后使用for循环将数组元素的数据逐一打印出来。
4.1.7 引用类型数组的初始化
引用类型数组中存储的是数据的引用地址。通过引用地址指向了实际存储数据的内存区域。下面通过定义一个Student类型的数组来演示引用类型数组的使用,如例4-6所示。
例4-6 Demo0406.java
1 package com.aaa.p040107;
2
3 class Student{ // 学生类
4 String name; // 姓名
5 int age; // 年龄
6 }
7
8 public class Demo0406 {
9 public static void main(String[] args) {
10 Student[] stus = new Student[2]; // 创建长度为2的学生数组
11 stus[0] = new Student(); // 为第一个数组元素存储学生对象
12 stus[0].name = "张三"; // 设置学生对象的属性
13 stus[0].age = 20;
14
15 stus[1] = new Student(); // 为第二个数组元素存储学生对象
16 stus[1].name = "李四"; // 设置学生对象的属性
17 stus[1].age = 18;
18
19 for(Student s : stus) { // 使用foreach循环输出学生对象数据
20 System.out.println(s.name + " " + s.age);
21 }
22 }
23 }
程序运行结果如下:
张三 20
李四 18
在例4-6中,先定义了一个长度为2的Student类型的数组。接着,为每个数组元素存储一个学生类型的对象,并为存储的学生对象设置属性。然后,使用foreach循环输出数组元素中的学生对象,将学生的姓名和年龄打印出来。
为了便于大家更好地理解引用类型数组存储数据的特点。下面通过一个图例对引用类型数组的使用进行说明,如图4.3所示。
(a)引用类型数组的初始化 (b)引用类型数组存储数据
图4.3 引用类型数组的数据存储方式
图4.3中,左图是引用类型数组定义并初始化的情况,引用类型数组的元素默认值都为null,不指向任何数据。右图中,定义了学生类型的数组,数组长度为2,数组包含两个元素。每个数组元素都存储了一个地址,分别指向不同的学生对象。
4.2 二维数组
一维数组主要用于存储线性数据,比如存储某校某年级所有学生一门课程的成绩,这种数据存储的结构是单个维度的。但是在实际应用中,一维数组并不能满足所有需求,比如存储某校某年级所有学生两门课程的成绩。所以,Java中提供了多维数组,但是Java中并没有真正的多维数组结构,它的多维数组的本质是让数组元素再存储一个数组,从而构成多维数组的结构。本节将以二维数组为例讲解多维数组的用法。二维数组的数据结构类似于一张表,包含行和列,下面展开详细讲解。
4.2.1 二维数组的创建
二维数组的结构可以看作是一张表,其中包含行和列,分别对应第一维度和第二维度。如图4.4所示,是一个3行2列的二维数组的存储结构示意图。
图4.4 3行2列二维数组结构
在Java中,声明二维数组时,需要使用两个中括号进行定义,在分配二维数组元素空间时,需要指明两个维度的数组长度。二维数组的创建和一维数组类似,也需要进行数组声明和数组空间分配,创建二维数组的语法如下:
数据类型[][] 数组名; // 声明二维数组变量
数组名 = new 数据类型[第一维长度][第二维长度]; // 为二维数组分配空间
当然也可以将数组声明和数组的内存分配用一行代码定义:
数据类型[][] 数组名 = new 数据类型[第一维长度][第二维长度]; // 声明二维数组并分配空间
根据二维数组的语法,创建一个3行2列的整型二维数组的示例代码如下:
int[][] scores; // 声明二维数组变量scores
scores = new int[3][2]; // 为二维数组分配3行2列的内存空间
在上述示例代码中,第1行声明了一个整型二维数组变量scores,这个变量未指向任何数组空间。接着,第2行代码先通过new运算符创建了一个3行2列的整型二维数组空间。然后,将二维数组赋值给scores变量,因此 scores变量存储了整型二维数组的地址。通过scores变量可以访问二维数组对象空间的每个元素。对于二维数组的声明和内存分配,如图4.5所示。
(a)声明整型二维数组变量但未指向数组对象空间 (b)声明整型二维数组变量并指向二维数组对象空间
图4.5 二维数组声明和内存分配
图4.4中,左图声明了一个整型二维数组变量scores,因为没有指向任何数组对象,所以scores变量存储的内容是null。右图在堆内存中创建了一个3行2列的整型二维数组。右图的scores变量存储了这个二维数组的首地址。二维数组的第一维是一个长度为3的一维数组,这个一维数组的每个空间都指向到另外一个长度为2的一维数组,由此构成了二维数组结构。通过scores变量使用两个维度的下标可以访问到二维数组中的每个数组元素。
4.2.2 二维数组的内存分配
二维数组创建之后,就可以存储数据到二维数组的元素空间中。二维数组元素的赋值方式与一维数组类似,也是3种方式:静态初始化、动态初始化、通过数组下标为数组赋值。
1.静态初始化
二维数组的静态初始化就是在声明数组时,由开发者显示指定二维数组元素的初始值。因为二维数组有两个维度,所以在设置数据的时候,要使用两层大括号来体现两个维度的结构。如下是两种二维数组静态初始化的语法:
// 声明数组同时初始化,外面的大括号表示第一维元素,里面的大括号表示第二维元素
数据类型[][] 数组名 = {{数据1,数据2,…},{数据1,数据2,…},…};
// 先声明二维数组,然后进行初始化
数据类型[][] 数组名 ;
数组名 = new 数据类型[][]{{数据1,数据2,…},{数据1,数据2,…},…};
上述语法中,二维数组静态初始化时,初始化的数据要写在两层大括号中,这是与一维数组不同的地方。第1层大括号中定义的是第一维的数组元素,这些数组元素本身又是一个数组,元素之间以逗号分隔。第2层大括号中存储的是实际的数据内容,多个数据之间也以逗号分隔。另外,在第2种静态初始的语法中,右边表达式中的“[ ][ ]”内,也不允许写数组长度,否则会发生语法错误。
二维数组静态初始化示例代码如下:
int[][] scores = {{78,65},{98,79},{87,89}}; // 声明数组并进行数组静态初始化
int[][] scores; // 先声明数组变量
scores = new int[][]{{78,65},{98,79},{87,89}}; // 再进行静态初始化
2.动态初始化
二维数组进行动态初始化时,开发者需要指定两个维度的数组长度,然后由系统自动为数组元素分配初始值。二维数组动态初始化的语法格式如下:
数据类型[][] 数组名 = new 数据类型[第一维数组长度][第二维数组长度];
二维数组在进行动态初始化后,程序会根据指定的两个维度的数组长度,创建对应的数组元素空间,并为每个数组元素空间设置初始值。
二维数组动态初始化的代码示例如下:
int[][] scores = new int[3][2]; // 创建3行2列的整形二维数组,元素的初始值都是0
String[][] names = new String[3][2];// 创建3行2列的字符串二维数组,元素的初始值都为null
3.通过数组下标为数组赋值
在二维数组创建之后,可以使用数组名结合二维数组行列下标的方式,为二维数组空间中的每个元素赋值。使用二维数组下标赋值的语法格式如下:
数据类型[][] 数组名 = new 数据类型[第一维数组长度][第二维数组长度];
数组名[第一维度下标][第二维度下标] = 数值;
在通过数组下标为数组元素赋值的时候,每个维度的数组下标的取值范围从0到对应维度的数组长度减1为止。下标超出这个范围,会发生“ArrayIndexOutOfBoundsException”数组下标越界的异常。
通过数组下标为元素赋值的示例代码如下:
String[][] names = new String[3][2]; // 声明3行2列字符串二维数组
names[0][0] = "张三"; // 通过2个维度的下标为每个数组元素赋值
names[0][1] = "李四";
names[1][0] = "王五";
names[1][1] = "赵六";
names[2][0] = "孙七";
names[2][1] = "钱八";
4.2.3 嵌套循环存取二维数组
二维数组有两个维度,在访问数组元素的时候要通过两个下标来访问。因为数组的下标具有连续性的特点,所以可以通过循环嵌套的方式来访问二维数组的每个元素。
接下来,通过循环嵌套的方式为一个3行2列的二维数组赋值,并用循环嵌套的方式输出存入的数据,如例4-7所示。
例4-7 Demo0407.java
1 package com.aaa.p040203;
2 import java.util.Scanner;
3
4 public class Demo0407 {
5 public static void main(String[] args) {
6 Scanner sc = new Scanner(System.in); // 定义输入扫描器对象,用于接受键盘输入
7 // 定义3行2列的二维数组,存储3名学生的2门成绩
8 int[][] scores = new int[3][2];
9 // 使用嵌套循环的方式为数组赋值, scores.length获取第一维数组的长度
10 for(int i = 0; i < scores.length; i++) {
11 // scores[i].length获取第二维数组的长度
12 for(int j = 0; j < scores[i].length; j++) {
13 System.out.println("请输入第" + ( i + 1 ) + "个学生的第" + ( j + 1 ) +
14 "门课成绩:");
15 scores[i][j] = sc.nextInt(); // 将输入的数据存入对应的数组位置
16 }
17 }
18
19 System.out.println("===================="); // 输出分隔符
20
21 // 使用嵌套循环的方式打印二维数组元素, scores.length获取第一维数组的长度
22 for(int i = 0; i < scores.length; i++) {
23 // scores[i].length获取第二维数组的长度
24 for(int j = 0; j < scores[i].length; j++) {
25 int score = scores[i][j]; // 获取当前遍历到的二维数组元素
26 // 输出分数
27 System.out.println("第" + (i + 1) + "个学生的第" + (j + 1) +
28 "门课成绩是:" + score);
29 }
30 }
31
32 }
33 }
程序运行结果如下:
请输入第1个学生的第1门课成绩:
23
请输入第1个学生的第2门课成绩:
34
请输入第2个学生的第1门课成绩:
45
请输入第2个学生的第2门课成绩:
56
请输入第3个学生的第1门课成绩:
67
请输入第3个学生的第2门课成绩:
87
====================
第1个学生的第1门课成绩是:23
第1个学生的第2门课成绩是:34
第2个学生的第1门课成绩是:45
第2个学生的第2门课成绩是:56
第3个学生的第1门课成绩是:67
第3个学生的第2门课成绩是:87
在例4-7中,先定义了一个输入扫描器对象(Scanner),用于接受从键盘输入的数据。接着,定义了一个3行2列的整型二维数组,用于存储3个学生两门课程的成绩。然后使用嵌套循环实现二维数组元素的赋值。scores.length是第1层循环的终止条件,它表示的是第一维度数组的长度。scores[i].length是第二层循环的终止条件,它表示的是第二维度数组的长度。在第2层循环内部,使用scores[i][j]表示访问到的某个二维数组元素,用于存储用户通过键盘输入的数据。嵌套循环结束后,这个二维数组的所有元素都会被赋值。接下来通过另外一个嵌套循环,将二维数组内的所有元素逐一打印输出。
知识点拨:Java中的多维数组本质上是在数组中存储数组,是在一维数组的基础上衍生出来的,因此理论上可以定义任何维度的数组。定义二维数组的时候需要使用两个中括号"[ ][ ]",以此类推,定义三维、四维数组只要定义对应个数的中括号即可。
4.2.4 非对称型数组
前面讲的二维数组用的都是矩阵数组,也就是数组的第二维度的长度都是一样的,是等行等列的对称结构。但是,Java中还有一种非对称的数组结构,被称为非对称型数组。对称型数组和非对称型数组的结构如图4.6所示。
(a)对称型数组(矩形数组) (b)非对称型数组(不规则数组)
图4.6 对称型数组和非对称型数组
对称型的3行4列数组有12个元素,但是非对称型数组的元素个数是不确定的。这里,我们定义一个非对称型数组,并进行静态初始化,然后输出一维数组元素指向的数组中的最后一个元素,示例代码如下:
int[][] scores = {{1,2,3},{5,6,7,8},{9,10}}; // 用静态初始化的方式定义非对称型数组
System.out.println(scores[0][2]); // 输出第一维下标为0的数组的最后一个元素
System.out.println(scores[1][3]); // 输出第一维下标为1的数组的最后一个元素
System.out.println(scores[2][1]); // 输出第一维下标为2的数组的最后一个元素
在上述示例代码中,声明的二维数组,第一维长度是3。但是,第二维长度各不相同,长度分别是:3,4,2。因此,第一维数组元素关联的数组的最后一个元素的下标是各不相同的。
4.3 数组的排序与查找
使用数组处理数据的时候,除了要对数据进行存储,大部分时候需要对数据进行查找和筛选,而查找数据的时候又往往需要对数据进行排序处理。比如,要找一个分数最高的成绩,或者年龄最小的学生,这时就需要在查找的过程中进行数据排序。
4.3.1 数组元素排序
对数组中的数据进行排序的算法有很多种,对于初学者而言,“冒泡排序”是最简单易懂的方法。冒泡排序的核心特点,就是先比较相邻两个元素的大小,然后根据排序规则进行换位。
如图4.7所示,是对一维数组中的5个数字:56,32,8,76,12进行冒泡排序,要求排序之后,数组中的数据按照从小到大的次序排列。
图4.7 冒泡排序
图4.7中,在开始排序时,5个数字没有按从小到大的次序进行排列。接着开始进行第1轮排序,排序的时候依次对相邻的两个数字比较大小,如果前面的数字大于后面的数字,则将这两个数字交换位置,否则就不进行换位。参与排序的5个数字全都比较一遍并做了相应的换位之后,数值最大的76排在了最后的位置上,这个数字不再参与下一轮的比较。第1轮排序一共进行了4次比较和换位。在第2轮排序时,排除了上一轮的最大值76后,只需要对4个数字进行排序。排序的过程依旧是比较和换位。最终经过3次比较,找到了本轮的最大值56。第3轮排序时,将前两轮的最大值72和56排除掉,只需要对剩下的3个数字进行排序。最终经过2次比较和换位,找到了本轮的最大值32。第4轮排序时,将前3轮的最大值72、56、32排除掉,只需要对剩下的2个数字进行排序,最终经过1次比较找到了本轮的最大值12。经过4轮排序,5个数字实现了从小到大的排列。
经过上面的排序过程可以发现,排序的轮数等于参与排序的数字的个数减1。而每一轮排序的次数等于本轮参与排序的数字个数减1。这是因为每一轮排序都会找到本轮的最大值,而在下一轮排序时这个最大值不再参与比较,所以随着排序轮数的增加,参与比较的数字在减少,这是冒泡排序的一个基本规律。
在了解了冒泡排序的基本原理后,下面用代码实现冒泡排序的过程,如例4-8所示:
例4-8 Demo0408.java
1 package com.aaa.p040301;
2
3 public class Demo0408 {
4 public static void main(String[] args) {
5 int[] nums = {56, 32, 8, 76, 12}; // 定义要排序的整形数组
6 System.out.println("=========排序前===========");
7 for(int n : nums) { // 循环输出初始的数组数据
8 System.out.print(n+" ");
9 }
10 System.out.println(); // 换行
11 // 使用双重循环实现冒泡排序,外层循环控制排序比较的轮数
12 for(int i = 1;i < nums.length; i++) {
13 for(int j = 0;j < nums.length - i;j++) { // 内层循环控制每一轮比较的次数
14 if(nums[j] > nums[j + 1]) { // 每次比较相邻的两个元素的大小
15 // 若前元素大于后元素,则交换位置,先将前元素的数据存入临时变量
16 int temp = nums[j];
17 nums[j] = nums[j + 1]; // 将后元素的数据存到前元素中
18 nums[j + 1] = temp; // 将临时变量数据存储到后元素中
19 }
20 }
21 }
22 System.out.println("=========排序后===========");
23 for(int n : nums) { // 循环输出排序后的数组数据
24 System.out.print(n + " ");
25 }
26 }
27 }
程序运行结果如下:
=========排序前===========
56 32 8 76 12
=========排序后===========
8 12 32 56 76
在例4-8中,先定义了一个整型一维数组,存储了要排序的5个数字。接着使用循环将排序前的这5个数字打印输出,用于和排序后的结果做对比。然后,使用嵌套的for循环实现冒泡排序的算法过程。在嵌套for循环中,外层循环用于控制排序比较的轮数。内层循环用于控制每轮排序比较的次数。在内层循环中,每次做比较时,如果参与比较的两个元素,前面的元素比后面的元素大,则要进行换位。换位的时候,先定义一个临时变量,用于存储参与比较的前面的数组元素,接着把后面的元素存入前面的元素中,最后再把临时变量存储的数据存入后面数组元素中,从而完成元素换位。嵌套循环执行完成之后,数组元素就完成了排序过程。最后,使用循环输出排序后的数组数据。
编程技巧:对于初学者而言,冒泡排序的算法代码不太容易掌握。大家只要记住下面四句话,那么冒泡排序就很容易写出来:N元数组冒泡序,两两相比小前移。外层循环从1始,内层循环减i去。
4.3.2 数组元素的查找
通过排序可以将数组元素排出大小顺序,排序的目的是为了更有效地查找数据。而对于数据的查找,也有很多算法,本节主要讲解顺序查找和二分查找。
1.顺序查找
顺序查找法是最简单的查找方法。可以对数组中的元素按照下标依次对比,直到查找到目标元素或者将所有数组元素遍历完毕为止。顺序查找法的效率较低,比如在N个元素中查找目标数据,平均要查找N/2次。所以,顺序查找法一般用于对少量数据进行查找,或者对未排序数据进行查找。
接下来,演示从有10个元素的整型数组中,使用顺序查找法查找数字13,找到则输出元素在数组中的下标,找不到则进行提示,如例4-9所示。
例4-9 Demo0409.java
1 package com.aaa.p040302;
2
3 public class Demo0409 {
4 public static void main(String[] args) {
5 // 定义长度为10的整形数组,并初始化
6 int[] nums = {34, 32, 45, 67, 98, 43, 31, 47, 13, 22};
7 int searchNum = 13; // 定义要查找的目标数据
8 int index = -1; // 定义变量记录查找到的目标数据位置
9 for(int i = 0; i < nums.length; i++) { // 循环遍历数组,用顺序查找法查找目标数据
10 if(nums[i] == searchNum) { // 判断遍历的当前元素和目标数据是否相等
11 index = i; // 如果相等则记录目标数据在数组中的位置
12 break; // 结束循环
13 }
14 }
15 if(index == -1) { // 循环结束后,判断记录目标数据位置是否为-1
16 // 如果记录的位置为-1,说明没有找到数据
17 System.out.println("在数组中没有要找到的目标数据");
18 }else {
19 // 如果记录的位置不为-1,则说明找到了目标数据
20 System.out.println("找到了目标数据,位置是:" + index);
21 }
22
23 }
24 }
程序运行结果如下:
找到了目标数据,位置是:8
在例4-9中,先定义了一个整型一维数组,存储了10个数字。接着,定义了一个变量存储要查找的目标数据13。然后,定义index变量用于记录查找结果,index变量的初始值设为-1。接着使用循环逐一获取数组中的元素与目标数据进行比较。如果数组元素与目标数据相等,就说明在数组中找到了目标数据,则用index存储当前数组元素的下标位置,然后退出循环。在循环结束后判断index的值,如果index值不是-1,则说明在数组元素中找到了目标数据,index存储了目标数据在数组中的位置,则将该位置打印输出。否则,说明数组中不存在查找的目标值,则打印输出没有找到的提示。
2.二分查找
二分查找法是查找效率比较高的查找算法,该方法的核心是在数组数据有序的基础上,在数组中标记低位和高位以限定查找范围,并用查找范围内的中间位置的数据和目标数据进行比较,不断调整低位和高位的索引位置从而缩小查找范围,最终找到目标值。如果在N个数据的范围内进行二分查找,则平均会执行log2N+1次查找比较。
如图4.8所示,是在长度为10的整型数组中,使用二分查找法,查找数据47的过程。
图4.8 二分查找法
图4.8中,初始定义了长度为10的整型数组,并存储了10个整数。然后,对数组进行排序。接着,按照二分查找法的算法规则,在数组中标记了低位(low)和高位(high)的索引位置,低位的索引位置初始为0,高位的索引位置初始为9(数组长度减1)。然后,通过低位和高位索引位置计算出一个中间位置,计算方法是:中间位置=(低位+高位)/2。根据这个公式计算得出中间位置的索引是4,此时使用该位置上的数据34和目标数据47做对比,目标数据47大于34,那么根据二分查找法的规则,为了缩小查找范围,要将低位索引变为中间索引加1,改变后低位索引的值就是5。接着使用改变后的低位索引和高位索引,重新计算得出一个新的中间位置(5+9)/2 = 7,此时中间位置7对应的数据是47,这个就是查找的目标数据。到此,二分查找法的整个过程结束。
接下来,使用代码演示的二分查找法的具体过程,如例4-10所示。
例4-10 Demo0410.java
1 package com.aaa.p040302;
2
3 public class Demo0410 {
4 public static void main(String[] args) {
5 // 定义长度为10的整形数组,并初始化
6 int[] nums = {34, 32, 45, 67, 98, 43, 31, 47, 13, 22};
7 System.out.println("=========排序前===========");
8 for(int n : nums) { // 循环输出排序前的数组数据
9 System.out.print(n+" ");
10 }
11 System.out.println(); // 输出换行
12 // 使用双重循环实现冒泡排序,外层循环控制排序比较的轮数
13 for(int i = 1;i < nums.length;i++) { // 内层循环控制每一轮比较的次数
14 for(int j = 0; j < nums.length - i;j++) { // 每次比较相邻的两个元素的大小
15 if(nums[j] > nums[j + 1]) {
16 // 如果前元素大于后元素,则交换位置,先将前元素的数据存入临时变量
17 int temp = nums[j];
18 nums[j] = nums[j + 1]; // 将后元素的数据存到前元素中
19 nums[j + 1] = temp; // 将临时变量数据存储到后元素中
20 }
21 }
22 }
23
24 System.out.println("=========排序后===========");
25 for(int n : nums) { // 循环输出排序后的数组数据
26 System.out.print(n + " ");
27 }
28 System.out.println(); // 输出换行
29 System.out.println("=========使用二分查找法===========");
30 // 使用二分法查找数据
31 int searchNum = 47; // 定义要查找的目标数据
32 int index = -1; // 定义变量记录查找到的目标数据的位置
33 int low = 0; // 定义低位索引变量,初始为0
34 int high = nums.length - 1; // 定义高位索引变量,初始为数组长度减1
35 int middle = -1; // 定义中间位置变量,初始为-1
36 do { // 通过循环实现二分查找过程
37 middle = (low + high) / 2; // 计算中间位置
38 if(nums[middle] == searchNum) {// 使用中间位置对应的数据和目标数据比较
39 index = middle; // 如果两个数据相等,则用index存储中间位置
40 break; // 退出循环
41 }
42 // 如果中间数据大于目标数据,则将高位索引设置为中间位置减1
43 if(nums[middle] > searchNum) {
44 high = middle - 1;
45 }else {
46 low = middle + 1; // 否则,将低位索引设置为中间位置加1
47 }
48 }while(low <= high); // 循环条件是低位索引位置小于高位索引位置
49 // 输出二分查找结果
50 if(index == -1) {
51 // 如果index记录的位置是-1,说明没有找到目标值
52 System.out.println("在数组中没有找到目标值");
53 }else{
54 // 如果index记录的位置不是-1,说明找到了目标值
55 System.out.println("找到了目标值:" + searchNum + "它的索引位置是:" + index);
56 }
57 }
58 }
程序运行结果如下:
=========排序前===========
34 32 45 67 98 43 31 47 13 22
=========排序后===========
13 22 31 32 34 43 45 47 67 98
=========使用二分查找法===========
找到了目标值:47 它的索引位置是:7
在例4-10中,先定义了长度为10的整形数组,并存储了10个整数。接着,输出了排序前的数组元素。然后,对数组进行了排序,并输出了排序后的数组元素。接着,定义了二分查找法需要的相关变量。其中,低位索引变量的初始值是0,高位索引变量的初始值是9(数组长度减1)。然后,使用do...while循环实现二分查找法的算法过程。在循环开始的时候,先通过低位索引和高位索引计算出中间位置。接着,使用中间位置上对应的数据和目标数据做对比,如果中间位置上的数据和目标数据相等,那就记录中间位置的索引值,查找到此结束。如果中间位置对应的数据比目标值大,那么根据二分查找法的规则,为了缩小查找范围,要将高位索引设置为中间位置索引减1。如果中间位置对应的数据比目标值小,那么要将低位索引设置为中间位置索引加1。只要低位索引小于等于高位索引,这个循环过程就一直持续,直到找到目标数据为止。最后,在循环结束后,使用记录目标数据索引值的index变量进行判断,如果该变量的值是-1,则提示没有找到目标数据,否则说明找到了目标数据,并将目标值在数组中的索引打印出来。
4.4 本章小结
Java中使用数组需要3个步骤:声明数组、创建数组对象空间、为数组元素赋值。
为数组元素赋值可以使用静态初始化、动态初始化、通过数组下标为数组元素赋值。
访问数组中的元素时,要使用数组名加下标的方式访问。
通过数组的length属性可以获取数组的长度。
除了使用for循环结合下标的方式遍历数组,还可以使用foreach方式遍历数组。
基本类型的数组,其数组元素空间存储的都是数据本身。
引用类型的数组,其数组元素空间存储的是数据的地址。
二维数组就是让数组元素再存储一个数组,二维数组有两个维度对应行和列。
二维数组的内存分配和一维数组类似,也有3种方式:静态初始化、动态初始化、使用数组下标赋值。
对二维数组而言,非对称型数组是指数组的第二维的长度各不相等。
冒泡排序的算法核心是相邻两个元素进行比较和换位
对数组元素查找的常用方法有:顺序查找法和二分查找法
4.5 理论测试与实践练习
1.填空题
1.1 数组的元素通过 来访问,访问数组的长度用 。
1.2 JVM将数组的存储在 (堆或栈)中。
1.3 数组下标访问超出索引范围时抛出 异常。
1.4 数组创建后其大小 (能/不能)改变。
1.5 Java中数组的最小下标是 。
2.选择题
2.1 定义了一维int类型数组a[10]后,下面错误的引用是( )
A.a[0]=1; B.a[10] = 2; C.a[0] = 5*2; D.a[1] = a[2]*a[0];
2.2 下面二维数组初始化语句中 ,正确的是( )
A.float b[2][2]={0.1,0.2,0.3,0.4};
B.int a[][]={{1,2},{3,4}};
C.int a[2][] = {{1,2},{3,4}};
D.float a[2][2]={0};
2.3 数组的第3个元素表示为( )
A.a(3) B.a[3] C.a(2) D.a[2]
2.4 通过哪些方式能够为数组赋值( )
A.静态初始化 B.动态初始化 C.使用数组下标赋值 D.声明数组变量
2.5 关于字符串类型的数组,说话正确的是( )
A.其数组的默认值是’’ B.其数组的默认值是null
C.数组的长度可以任意变化 D.可以存储整形数值
3.思考题
3.1 请简述什么时候为数组分配内存?
3.2 请简述数组一旦被创建,大小能不能改变?
3.3 请简述顺序查找法的过程?
4.编程题
4.1 定义整形数组,存放5个学生的成绩【成绩值自己设定】,将成绩从大到小排序,获得成绩之和,平均成绩,最小成绩,最大成绩。
4.2定义长度为10的整形数组,并存储10个数字。再通过键盘输入一个数字,使用二分查找法在数组中查找输入的数字是否存在。
- 点赞
- 收藏
- 关注作者
评论(0)