第5章〓数组 数组是几乎所有程序设计语言都提供的一种数据存储结构。Java语言的数组是一种引用数据类型,即数组是对象。数组在程序设计中具有广泛应用,它通常用来存储和操作一组类型相同的数据。本章首先介绍数组的创建和使用,然后介绍数组的有关应用、Arrays类的使用和二维数组。 本章内容要点 ■创建和使用数组。 ■可变参数方法。 ■数组的排序和查找。 ■java.util.Arrays类。 ■二维数组及应用。 5.1创建和使用数组 数组(array)是一种数据结构,它用来存储一组类型相同的数据,它是一个引用类型变量,通过下标来访问数组中的元素。下面介绍如何声明、初始化和使用数组。 5.1.1声明和创建数组 使用数组一般需要两个步骤: ①声明数组,即声明数组名称和元素的数据类型; ②创建数组,即为数组元素分配存储空间。 1. 声明数组 使用数组之前需要声明。声明数组就是告诉编译器数组名和数组元素类型。数组声明可以使用下面两种等价形式。 elementType []arrayName; elementType arrayName[]; 这里,elementType为数组元素类型,它可以是基本数据类型(如boolean或char型),也可以是引用数据类型(如String或Employee型等)。arrayName为数组名,它是一个引用变量。方括号指明变量为数组变量,它既可以放在变量前面也可以放在变量后面。推荐放在变量前面,这样更直观。 例如,下面声明了marks和staffs两个数组。 double []marks; Employee []staffs; 数组声明不能指定数组元素的个数。下面的代码发生编译错误。 double [5]marks; 上面声明的数组,它们的元素类型分别为double和Employee型。在Java语言中,数组是引用数据类型。也就是说,数组是一个对象,数组名就是对象名(或引用名)。数组声明实际上是声明一个引用变量。如果数组元素为引用类型,则该数组称为对象数组,如上面的staffs就是对象数组。所有数组都继承了Object类,因此可以调用Object类的所有方法。 2. 创建数组 数组声明仅仅声明一个数组对象引用,而创建数组是为数组的每个元素分配存储空间。创建数组使用new语句,一般格式为: arrayName = new elementType[size]; 该语句的功能是分配size个elementType型的存储空间,并通过arrayName来引用。例如: marks = new double[5]; // 数组包含5个double型元素 staffs = new Employee[3]; // 数组包含3个Employee型元素 Java数组的大小可以在运行时动态指定。比如,下面的代码是合法的。 int n = 5; double []marks = new double[n]; // 也是创建包含5个double型元素的数组 数组的声明与创建可以写在一条语句中。例如: double []marks = new double[5]; Employee []staffs = new Employee[3]; 当用new 运算符创建一个数组时,系统就为数组元素分配了存储空间,这时系统根据指定的长度创建若干存储空间并为数组的每个元素指定默认值。数值型数组元素默认值是0,字符型元素的默认值是'\u0000',布尔型元素的默认值是false。如果数组元素是引用类型,其默认值是null。 上面两条语句分别分配了5个double型和3个Employee型的空间,并且每个元素使用默认值初始化。上面两条语句执行后的结果如图51所示。数组marks的每个元素都被初始化为0.0,而数组staffs的每个元素被初始化为null。 对于引用类型数组(对象数组),还要为每个数组元素分配引用空间,也就是创建每个元素对象。对于staffs数组,创建元素的代码如下。 staffs[0] = new Employee("张三", 28, 8000); staffs[1] = new Employee("王五", 30, 8500); staffs[2] = new Employee("李四", 26, 6000); 上面的语句执行后staffs数组的结果如图52所示。 图51marks数组和staffs数组 图52staffs数组创建后的结果 5.1.2访问数组元素 声明了一个数组,并使用new运算符为数组元素分配内存空间后,就可以使用数组中的每一个元素。数组元素的使用方式如下: arrayName [index] 其中,index为数组元素的下标或索引,下标从0开始,到数组的长度减1。例如,上面定义的staffs数组定义了3个元素,所以只能使用staffs[0]、staffs[1]和staffs[2]这三个元素。数组一经创建,大小不能改变。 数组作为对象提供了一个length成员变量,它表示数组元素个数,访问该成员变量的方法为arrayName.length。 程序5.1演示了数组的使用和length成员的使用。 程序5.1ArrayDemo.java package com.boda.xy; public class ArrayDemo { public static void main(String[] args) { var marks = new double[5]; marks[0] = 79; marks[1] = 84.5; marks[2] = 63; marks[3] = 90; marks[4] = 98; System.out.println(marks[2]); System.out.println(marks.length); for (var i = 0; i < marks.length; i++) { System.out.print(marks[i] + " " ); } } } 程序运行结果如下: 63.0 5 79.0 84.5 63.0 90.0 98.0 为了保证安全性,Java运行时系统要对数组元素的范围进行越界检查,若数组元素下标超出范围,将抛出ArrayIndexOutOfBoundsException运行时异常。例如,下面的代码抛出异常。 System.out.println(marks[5]); 5.1.3数组初始化器 声明数组同时可以使用初始化器(initializer)对数组元素初始化,它是在一对花括号中给出数组的每个元素值。这种方式适合数组元素较少的情况,这种初始化也称为静态初始化。 var marks = new double[]{79, 84.5, 63,90, 98}; var staffs = new Employee[]{ new Employee("张三", 28, 800), new Employee("王五", 30, 8500), new Employee("李四", 26, 6000), }; 用这种方法创建数组不能指定大小,系统根据元素个数确定数组大小。另外,可以在最后一个元素后面加一个逗号,以方便扩充。 上面两个语句还可以写成如下形式: double []marks = {79, 84.5, 63,90, 98}; Employee []staffs = { new Employee("张三", 28, 800), new Employee("王五", 30, 8500), new Employee("李四", 26, 6000), }; 5.1.4增强的for循环 如果程序只需顺序访问数组中的每个元素,可以使用增强的for循环。增强的for循环可以用来迭代数组和对象集合的每个元素,也叫foreach循环。它的一般格式为: for(type identifier:expression) { // 循环体 } 该循环的含义为: 将expression(数组或集合)中的每个元素依次存入identifier变量,然后执行一次循环体中的语句。这里,type为数组或集合中的元素类型。expression必须是数组或集合对象。 下面使用增强的for循环实现求marks数组中各元素的和,代码如下: double sum = 0; for(var score :marks){ sum = sum + score; } System.out.println("总成绩=" + sum); 5.2数组的应用 本节介绍几个数组的应用,包括数组元素的复制、数组参数和返回值、数组的排序,以及可变参数方法等。 5.2.1数组元素的复制 经常需要将一个数组的元素复制到另一个数组中。首先会想到将数组元素一个一个复制到目标数组中。设有一个数组source,其中有4个元素,现在定义一个数组target,与原来数组类型相同,元素个数相同。使用下面的方法将源数组的每个元素复制到目标数组中。 int[] source = {10,30,20,40}; // 源数组 int[] target = new int[source.length]; // 目标数组 for(var i = 0; i < source.length; i++){ target[i] = source[i]; } 除上述方法外,还可以使用System类的arraycopy()方法,格式如下: public static void arraycopy(Object src, int srcPos, Object dest, int destPos,int length) 其中,参数src为源数组名,srcPos为源数组的起始下标,dest为目的数组名,destPos为目的数组下标,length为复制的数组元素个数。下面的代码实现将source中的每个元素复制到数组target中。 int[] source = {10,30,20,40}; int[] target = new int[source.length]; System.arraycopy(source, 0, target, 0, 4); 使用arraycopy()方法可以将源数组的一部分元素复制到目标数组中。但要注意,如果目标数组不足以容纳源数组元素,会抛出异常。 程序5.2ArrayCopyDemo.java package com.boda.xy; public class ArrayCopyDemo{ public static void main(String[] args){ int[] a = {1,2,3,4}; int[] b ={8,7,6,5,4,3,2,1}; int[] c = {10,20}; try{ System.arraycopy(a, 0, b, 0, a.length); // 下面的语句发生异常,目标数组c容纳不下原数组a的元素 System.arraycopy(a, 0, c, 0, a.length); }catch(ArrayIndexOutOfBoundsException e){ System.out.println(e); } for(var elem: b){ System.out.print(elem+" "); } System.out.println(); for(var elem: c){ System.out.print(elem+" "); } System.out.println("\n"); } } 运行程序,输出结果如下: java.lang.ArrayIndexOutOfBoundsException 12344321 1020 图53将source赋值给 target的结果 注意不能使用下列方法试图将数组source中的每个元素复制到target数组中。 int[] source = {10,30,20,40}; int[] target = source; // 这是引用赋值 上述两条语句实现对象的引用赋值,两个数组引用指向同一个数组对象,如图53所示。 5.2.2数组参数与返回值 数组对象可以作为参数传递给方法,例如下面的代码定义了一个求数组元素和的方法。 public static double sumArray(double array[]){ var sum = 0.0; for(var i = 0; i < array.length; i++){ sum = sum + array[i]; } return sum; } 注意由于数组是对象,因此将其传递给方法是按引用传递。当方法返回时,数组对象不变。但要注意,如果在方法体中修改了数组元素的值,则该修改反映到返回的数组对象。 一个方法也可以返回一个数组对象。例如,下面的方法返回参数数组的元素反转后的一个数组。 public static int[] reverse(int[] list){ var result = new int[list.length]; // 创建一个与参数数组大小相同的数组 for(int i = 0, j = result.length - 1; i < list.length; i++ , j--){ result[j] = list[i]; // 实现元素反转 } return result;// 返回数组 } 有了上述方法,可以使用如下语句实现数组反转。 int[] list = {6, 7, 8, 9, 10}; int[] list2 = reverse(list); 5.2.3可变参数的方法 Java语言允许定义方法(包括构造方法)带数量可变的参数,这种方法称为可变参数(variable argument)方法。具体做法是在方法参数列表的最后一个参数的类型名之后、参数名之前使用省略号(…)表示。例如: public static double average(double … values){ // 方法体 } 这里,参数values被声明为一个double型值的序列。其中参数的类型可以是基本类型,也可以是引用类型。对可变参数的方法,调用时可以为其传递任意数量的指定类型的实际参数。在方法体中,编译器将为可变参数创建一个数组,并将传递来的实际参数值作为数组元素的值,这就相当于为方法传递一个指定类型的数组。请看程序5.3。 程序5.3VarargsDemo.java package com.boda.xy; public class VarargsDemo{ public static double average(double … values){ var sum = 0.0; for(var value:values){ sum = sum + value; // 求数组元素之和 } double average = sum / values.length; return average; } public static void main(String[] args){ System.out.println(average(60,70,86)); // 输出:72.0 } } 程序定义了带可变参数的方法average(),它的功能是返回传递给该方法的多个double型数的平均值。该程序调用了average()方法并为其传递三个参数,输出结果为72.0。 在可变参数的方法中还可以有一般的参数,但是可变参数必须是方法的最后一个参数,例如,下面定义的方法也是合法的: public static double average(String name,double … values){ // 方法体 } 注意在调用带可变参数的方法时,可变参数是可选的。如果没有为可变参数传递一个值,那么编译器将生成一个长度为0的数组。如果传递一个null值,将产生一个运行时NullPointerException异常。 通常,对带一个数组参数或最后一个参数是数组的方法都可以使用可变参数。例如,Java应用程序的main()方法就可以写成下面的形式。 public static void main(String … args){} 5.2.4数组的查找 在程序设计中,查找是计算机程序设计中常见的功能。查找(searching)是在数组中寻找特定元素的过程。例如,一个数组可能存放学生某科考试成绩,现要求在其中查找是否有某个成绩(如99分)。有很多算法用于查找,本节讨论两种常用的算法: 线性查找(linear searching)和二分查找(binary searching)。 1. 线性查找法 线性查找法是将要查找的关键字key与数组中的元素逐个进行比较,直到在数组中找到与关键字相等的元素,或者查完所有元素也没有找到。如果查找成功,线性查找法返回与关键字相等的元素在数组中的下标,如果不成功,则返回-1。下面的linearSearch()方法在数组array中查找关键字key。 public static int linearSearch(int[] array, int key){ for (var i = 0; i < array.length ; i++){ if(array[i]==key) return i; } return -1; } 线性查找法用关键字与数组中的每一个元素进行比较。数组中的元素可以按任意顺序排列。在平均情况下,这种算法需要比较数组中一半的元素。由于线性查找法的执行时间随数组元素个数的增加而线性增加,所以对于大的数组来说其效率不高。 2. 二分查找法 二分查找法是另一种常见的查找法。使用二分查找法的前提条件是数组元素必须已经排序。不失一般性,假设数组按升序排序。二分查找法首先将关键字与数组的中间元素比较,有下面3种情况: (1) 如果关键字比中间元素小,则只需在前一半数组元素中查找。 (2) 如果关键字和中间元素相等,则查找成功,查找结束。 (3) 如果关键字比中间元素大,则只需在后一半数组元素中查找。 显然,二分查找法每比较一次就排除数组中一半的元素。假设用low和high分别记录当前查找的数组的第一个和最后一个下标。初始条件下,low为0,high为array.length-1。mid表示数组中间元素的下标,这样mid就是(low+high)/2。 public static int binarySearch(int[] array, int key){ int low = 0; int high = array.length-1; while (high >= low){ int mid = (low + high) / 2; if(key < array[mid]) high = mid -1; else if(key == array[mid]) return mid; else low = mid + 1; } return -low - 1; } 首先,关键字key与中间元素array[mid]比较,如果小于中间元素,将high设为mid-1; 如果等于中间元素,则匹配成功,返回mid; 如果大于中间元组,将low设为mid + 1。继续这样的查找,直到low > high或查找成功。如果low > high,则返回-low-1,low就是插入点。 5.3案例学习——数组起泡排序 1. 问题描述 在程序设计中,排序(sorting)是一种常见的操作。例如,一个数组可能存放某个学生的各科成绩,现要求将成绩按从低到高的顺序输出,这就需要为数组排序。排序有多种方法,如起泡排序、选择排序、插入排序、快速排序等。本案例采用起泡排序法(bubble sorting)对一个数组按升序排序。 2. 设计思路 假设给定下面的整型数组: int[]a = {75, 53, 32, 12, 46, 199, 17, 54}; 根据问题要求,采用起泡排序法对该数组升序排序,设计思路如下: 从数组的第一个元素开始,与它后面的元素比较,如果逆序(即a[j] > a[j+1]),则交换两个元素的位置,如果正序,则不交换。经过第一趟排序后,结果如下: 53, 32, 12, 46, 75, 17, 54, 199 可以看到,经过第一趟排序,找到了数组中的最大值,而经过比较后,小的元素交换到前面,就像水泡向上冒,起泡排序由此得名。第二趟排序时,只需比较到倒数第二个元素为止,即可找到次大的元素,因此这里使用二重循环结构。 3. 代码实现 根据上面的设计思路,可以将排序过程定义为一个方法,如bubbleSort(int[] a),参数是要排序的数组。在主方法中定义要排序的数组,然后调用排序方法即可。实现代码如程序5.4所示。 程序5.4BubbleSort.java package com.boda.xy; public class BubbleSort { public static void bubbleSort(int[] a){ for(var i = 0; i < a.length; i++) { for(var j = 0; j < a.length -i -1; j ++) { if(a[j] > a[j+1]) { int t = a[j]; a [j] = a[j+1]; a[j+1] = t; } } System.out.print("第 " + (i+1) + "趟结果:"); for(var n:a) { System.out.print(" " + n); } System.out.println(); } } public static void main(String[] args) { int[]nums = {75, 53, 32, 12, 46, 199, 17, 54}; //int[]nums = {199,75, 54,53, 46,32, 17, 12}; System.out.print("初始元素:"); for(var n:nums) { System.out.print(" " + n); } System.out.println(); bubbleSort(nums); 图54程序运行结果 } } 4. 运行结果 程序运行结果如图54所示,这里输出了每趟排序的结果。 使用下节讨论的java.util.Arrays类的sort()方法可以对任何类型元素的数组进行排序,还可以对数组执行查找和其他操作。 5.4java.util.Arrays类 java.util.Arrays类定义了若干静态方法对数组操作,包括对数组排序,在已排序的数组中查找指定元素,数组元素的复制,比较两个数组是否相等,将一个值填充到数组的每个元素中。上述操作都有多个重载的方法,可用于所有的基本数据类型和Object类型。 5.4.1数组的复制 使用Arrays类的copyOf()方法和copyOfRange()方法将一个数组中的全部或部分元素复制到另一个数组中。有10个重载的copyOf()方法,其中8个为各基本类型的,2个为对象类型的。下面给出几个方法的格式。 static boolean[] copyOf(boolean[] original,int newLength) static double[] copyOf(double[] original,int newLength) static <T> T[] copyOf(T[] original,int newLength) 这些方法的original参数是原数组,newLength参数是新数组的长度。如果newLength小于原数组的长度,则将原数组的前面若干元素复制到目标数组。如果newLength大于原数组的长度,则将原数组的所有元素复制到目标数组,目标数组的长度为newLength。 下面的代码创建了一个包含4个元素的数组,将numbers的内容复制到它的前三个元素中。 int[] numbers = {3, 7, 9}; int[] newArray = Arrays.copyOf(numbers, 4); 当然,也可以将新数组重新赋给原来的变量: numbers = Arrays.copyOf(numbers, 4); 与copyOf()类似的另一个方法是copyOfRange(),它可以将原数组中指定位置开始的若干元素复制到目标数组中。下面是几个方法的格式。 static boolean[] copyOfRange (boolean[] original,int from,int to) static double[] copyOfRange (double[] original,int from,int to) static <T> T[] copyOfRange (T[] original,int from,int to) 上述方法中,from参数指定复制的元素在原数组中的起始下标,to参数是结束下标(不包含),将这些元素复制到目标数组中。 char[] letter = {'a','b', 'c','d','e','f','g'}; letter = Arrays.copyOfRange(letter, 1, 5); 上述代码执行后,letter数组的长度变为4,其中包含'b'、'c'、'd'和'e'等4个元素。 5.4.2数组的排序 使用Arrays的sort()方法可以对数组元素排序。使用该方法的排序是稳定的(stable),即相等的元素在排序结果中不会改变顺序。对于基本数据类型,按数据的升序排序。对于对象数组的排序要求数组元素的类必须实现Comparable接口,若要改变排序顺序,还可以指定一个比较器对象。整型数组和对象数组的sort()方法格式如下。 static void sort(int[] a): 对数组a按自然顺序排序。 static void sort(int[] a,int fromIndex,int toIndex): 对数组a中的元素从起始下标fromIndex到终止下标toIndex的元素排序。 static void sort(Object[] a): 对数组a按自然顺序排序。 static void sort(Object[] a,int fromIndex,int toIndex): 对数组a中的元素从起始下标fromIndex到终止下标toIndex的元素排序。 static <T>void sort(T[] a,Comparator <?super T>c): 使用比较器对象c对数组a排序。 注意不能对布尔型数组排序。 由于在程序设计中经常使用排序,所以在java.util.Arrays类中提供几个重载的sort()方法,它们用于对int型、double型、char型、float型数组的排序。例如,下面的代码为double型数组和char型数组排序。 double[] numbers = {5.0, 4.4, 1.9, 2.9, 3.8, 3.5}; java.util.Arrays.sort(numbers); char[] chars = {'a', 'A', '4', 'F', 'D','&'}; java.util.Arrays.sort(chars); 下面的代码演示了对一个字符串数组的排序,对字符串排序是按字符的Unicode码排序的。 String[] ss = {"中国", "英国","法国","美国","俄罗斯"}; for(var i = 0;i<ss.length;i++){ System.out.print(ss[i]+" "); } System.out.println(); Arrays.sort(ss); // 对数组ss排序 for(var s : ss){ System.out.print(s + " "); } 代码输出结果为: 中国英国法国美国俄罗斯 中国俄罗斯法国美国英国 5.4.3元素的查找 对排序后的数组可以使用binarySearch()方法从中快速查找指定元素,该方法也有多个重载的方法,下面是对整型数组和对象数组的查找方法。 static int binarySearch(int[] a,int key) static int binarySearch(Object[] a,Object key) 查找方法根据给定的键值,查找该值在数组中的位置,如果找到指定的值,则返回该值的下标值。如果查找的值不包含在数组中,方法的返回值为(-插入点-1)。插入点为指定的值在数组中应该插入的位置。 例如,下面的代码输出结果为-3。 var a = new int[]{1,5,7,3}; Arrays.sort(a); // 返回a的元素是:1 3 5 7 var i = Arrays.binarySearch(a,4); System.out.println(i); // 输出:-3 在已排序的数组a中查找4,返回的结果-3(-2-1),它的含义要把4插入到数组a中,它的下标应该是2。 注意使用binarySearch()方法前,数组必须已经排序。 5.4.4数组的比较 使用Arrays的equals()方法可以比较两个数组,被比较的两个数组要求数据类型相同且元素个数相同,比较的是对应元素是否相同。对于引用类型的数据,如果两个对象e1和e2的值都为null或者e1.equals(e2),则认为e1与e2相等。 下面是布尔型数组和对象数组equals()方法的格式。 static boolean equals(boolean[] a,boolean[] b): 比较布尔型数组a与b是否相等。 static boolean equals(Object[] a,Object[] b): 比较对象数组a与b是否相等。 程序5.5给出了equals()方法的示例。 程序5.5EqualsTest.java package com.boda.xy; import java.util.*; public class EqualsTest { public static void main(String[] args) { int[] a1 = new int[10]; int[] a2 = new int[10]; Arrays.fill(a1, 47); Arrays.fill(a2, 47); System.out.println(a1.equals(a2)); // 输出:false System.out.println(Arrays.equals(a1, a2)); // 输出:true a2[3] = 11; System.out.println(Arrays.equals(a1, a2)); // 输出:false String[] s1 = new String[5]; Arrays.fill(s1, "Hi"); String[] s2 = {"Hi", "Hi", "Hi", "Hi", "Hi"}; System.out.println(Arrays.equals(s1, s2)); // 输出:true } } 使用数组对象的equals()方法比较两个引用是否相同。使用Arrays类的equals()方法比较两个数组对应元素是否相同。 5.4.5填充数组元素 调用Arrays类的fill()方法可以将一个值填充到数组的每个元素中,也可将一个值填充到数组的几个连续元素中。下面是向整型数组和对象数组中填充元素的方法。 static void fill(int[] a,int val): 用指定的val值填充数组a中的每个元素。 static void fill(int[] a,int fromIndex,int toIndex,int val): 用指定的val值填充数组中的下标从fromIndex开始到toIndex为止的每个元素。 static void fill(Object[] a,Object val): 用指定的val值填充对象数组中的每个元素。 static void fill(Object[] a,int fromIndex,int toIndex,Object val): 用指定的val值填充对象数组中的下标从fromIndex开始到toIndex为止的每个元素。 下面的代码创建一个整型数组,然后使用fill()方法为其每个元素填充一个两位随机整数。 var intArray = new int[10]; for(var i = 0;i < intArray.length;i++){ var num = (int)(Math.random()*90) + 10; Arrays.fill(intArray, i, i + 1, num); } for(var i : intArray) System.out.print(i+" "); } 下面是该段代码某次输出的结果: 58739234563213673098 提示除上述讨论的方法外,Arrays类中还提供了许多其他对数组操作的方法,请读者参考Java API文档。 5.5案例学习——桥牌随机发牌 1. 问题描述 桥牌是一种文明、高雅、竞技性很强的智力游戏,由4个人分两组玩。桥牌使用普通扑克牌去掉大小王后的52张牌,分为梅花(C)、方块(D)、红心(H)和黑桃(S)四种花色,每种花色有13张牌,从大到小的顺序为: A(最大)、K、Q、J、10、9、8、7、6、5、4、3、2(最小)。 打桥牌首先需发牌。本案例就是通过编程实现随机发牌。最后,按顺序显示输出4个玩家得到的牌。 2. 设计思路 根据问题描述,本案例的设计思路如下: (1) 可以使用一个有52个元素的数组存储52张牌。为了区分52张牌,使用不同的元素值即可,为了方便这里使用0~51。设元素值从0~12为黑桃,13~25为红桃,26~38为方块,39~51为梅花。在创建数组后为每个元素赋值,如下所示。 int [] deck = new int[52]; for(var i = 0; i < deck.length; i++) // 填充每个元素 deck[i] = i; (2) 为实现随机发牌,需要打乱数组元素的值,这里对每个元素随机生成一个整数下标,将当前元素与产生的下标的元素交换,循环结束后,数组中的元素值被打乱。 for(var i = 0; i < deck.length;i++){ // 随机产生一个元素下标0~51 int index = (int)(Math.random()*deck.length); int temp = deck[i]; // 将当前元素与产生的下标的元素交换 deck[i] = deck[index]; deck[index] = temp; } (3) 牌的顺序打乱后,4个玩家依次从52张牌中取出13张牌。第1个13张牌属于第1个玩家,第2个13张牌属于第2个玩家,第3个13张牌属于第3个玩家,第4个13张牌属于第4个玩家。最后要求每个玩家的牌按顺序输出,因此需要对每个玩家的牌排序,这里使用Arrays类的sort()方法,它可以对数组部分元素排序,比如对第3个玩家的牌排序,使用下面的代码。 Arrays.sort(deck,26,39); (4) 最后根据每张牌的数值转换为牌的名称(如K)。为此,定义两个String数组,如下所示。 String[] suits = {"","","",""}; String[] ranks = {"A","K","Q","J","10","9","8","7", "6","5","4","3","2"}; 根据下面的代码确定每张牌的花色和次序,然后输出。 String suit = suits[deck[i]/13]; // 确定花色 String rank = ranks[deck[i]%13]; // 确定次序 System.out.printf("%s%-3s",suit,rank); 3. 代码实现 该案例的完整实现代码如程序5.6所示。 程序5.6BridgeCards.java package com.boda.xy; import java.util.Arrays; public class BridgeCards { public static void main(String[]args){ int[]deck = new int[52]; String[] suits = {"","","",""}; String[] ranks = {"A","K","Q","J","10","9","8","7", "6","5","4","3","2"}; // 初始化每一张牌 for(var i = 0; i < deck.length;i++) deck[i] = i; // 打乱牌的次序 for(var i = 0; i < deck.length;i++){ // 随机产生一个元素下标0~51 int index = (int)(Math.random()*deck.length); int temp = deck[i]; // 将当前元素与产生的下标元素交换 deck[i] = deck[index]; deck[index] = temp; } Arrays.sort(deck,0,13); Arrays.sort(deck,13,26); Arrays.sort(deck,26,39); Arrays.sort(deck,39,52); // 显示所有52张牌 for(var i = 0; i < 52; i++){ switch(i%13) { case 0-> System.out.print("玩家"+(i/13 +1) +":"); } String suit = suits[deck[i]/13]; // 确定花色 String rank = ranks[deck[i]%13]; // 确定次序 System.out.printf("%s%-3s",suit,rank); if((i+1)%13 == 0) { System.out.println(); } } } } 4. 运行结果 案例运行结果如图55所示。 图55案例运行结果 从输出结果可以看到,每个玩家得到13张牌,并且每个玩家的牌按花色和大小进行了排序。 5.6二维数组 Java语言中的数组元素还可以是一个数组,这样的数组称为二维数组或数组的数组(array of array)。 5.6.1二维数组的定义 二维数组的使用也分为声明和创建数组两个步骤。 1. 二维数组声明 二维数组有下面三种等价的声明格式。 elementType[][] arrayName; elementType arrayName[][]; elementType[] arrayName[]; 其中,elementType为数组元素的类型,arrayName为数组名。推荐使用第一种方法声明二维数组。下面的语句声明了一个整型二维数组matrix和一个String型二维数组cities。 int [][] matrix; String [][] cities; 2. 创建二维数组 创建二维数组就是为二维数组的每个元素分配存储空间。系统先为高维分配引用空间,然后再顺次为低维分配空间。二维数组的创建也使用new运算符,分配空间有两种方法,下面是直接为每一维分配空间。 int [][]matrix = new int[2][3]; // 直接为每一维分配空间 图56matrix数组元素空间的分配 这种方法适用于数组的低维具有相同个数的数组元素。在Java中,二维数组是数组的数组,即数组元素也是一个数组。上述语句执行后创建的数组如图56所示,二维数组matrix有两个元素,matrix[0]和matrix[1],它们又都是数组,各有三个元素。图56中共有三个对象: matrix、matrix[0]和matrix[1]。 创建了二维数组后,它的每个元素被指定为默认值。上述语句执行后,数组matrix的6个元素值都被初始化为0。 在创建二维数组时,也可以先为第一维分配空间,然后再为第二维分配空间。 int[][]matrix = new int[2][]; // 先为第一维分配空间 matrix[0] = new int[3]; // 再为第二维分配空间 matrix[1] = new int[3]; 5.6.2数组元素的使用 访问二维数组的元素,使用下面的形式: arrayName[index1][index2] 其中,index1和index2为数组元素下标,可以是整型常数或表达式。同样,每一维的下标也是从0到该维的长度减1。 下面的代码给matrix数组的每个元素赋值: matrix[0][0] = 80; matrix[0][1] = 75; matrix[0][2] = 78; matrix[1][0] = 67; matrix[1][1] = 87; matrix[1][2] = 98; 下面的代码输出matrix[1][2]元素值: System.out.println(matrix[1][2]); 与访问一维数组一样,访问二维数组元素时,下标也不能超出范围,否则抛出异常。可以用matrix.length得到数组matrix的长度,结果为2,用matrix[0].length得到matrix[0]数组的长度,结果为3。 对二维数组的第一维通常称为行,第二维称为列。要访问二维数组的所有元素,应该使用嵌套的for循环。例如,下面的代码输出matrix数组中所有元素。 for(var i = 0; i < matrix.length; i ++){ for(var j = 0; j < matrix[0].length; j ++){ System.out.print(matrix[i][j] +" "); } System.out.println(); // 换行 } 同样,在访问二维数组元素的同时,可以对元素进行处理,比如计算行的和或者列的和等。 5.6.3数组初始化器 对于二维数组也可以使用初始化器在声明数组的同时为数组元素初始化。例如: int[][]matrix = {{15,56,20,-2}, {10,80,-9,31}, {76,-3,99,21},}; matrix数组是3行4列的数组。多维数组每一维也都有一个length成员表示数组的长度。matrix.length的值是3,matrix[0].length的值是4。 5.6.4实例——矩阵乘法 使用二维数组可以计算两个矩阵的乘积。如果矩阵A乘以矩阵B得到矩阵C,则必须满足如下要求: (1) 矩阵A的列数与矩阵B的行数相等。 (2) 矩阵C的行数等于矩阵A的行数,列数等于矩阵B的列数。 例如,下面的例子说明了两个矩阵是如何相乘的。 121-241×430-123521063=99166162613 在结果矩阵中,第1行第1列的元素是9,它通过下列计算得来: 1 × 4 + 2 × 2 + 1 × 1 = 9 即若矩阵Amn×Bnl = Cml,则 cij=∑nk=1aikbkj 其中,Amn表示m×n矩阵,cij是矩阵C的第i行j列元素。 程序5.7MatrixMultiply.java package com.boda.xy; public class MatrixMultiply { public static void main(String[]args){ int a[][]={{1,2,1}, {-2,4,1}}; int b[][]={ {4,3,0,-1}, {2,3,5,2}, {1,0,6,3}}; int c[][] = new int[3][4]; // 用一个三重循环计算矩阵乘法 for(var i = 0; i < 2; i++){ for(var j = 0; j < 4; j++) for(var k = 0; k < 3; k++) c[i][j] = c[i][j] + a[i][k] * b[k][j]; } // 输出矩阵结果 for(var i = 0; i < 2; i++){ for(var j = 0; j < 4; j++) System.out.print(c[i][j] + " "); System.out.println(); // 换行 } } } 程序运行结果为: 99166 162613 5.6.5不规则二维数组 Java的二维数组是数组的数组,对二维数组声明时可以只指定第一维的长度,第二维的每个元素可以指定不同的长度。例如: String [][]cities = new String[2][]; // cities数组有2个元素 cities[0] = new String[3]; // cities[0]数组有3个元素 cities[1] = new String[2]; // cities[1]数组有2个元素 这种方法适用于低维数组元素个数不同的情况,即每个数组的元素个数可以不同。对于引用类型的数组,除了为数组分配空间外,还要为每个数组元素的对象分配空间。 cities[0][0] = new String("北京"); cities[0][1] = new String("上海"); cities[0][2] = new String("广州"); cities[1][0] = new String("伦敦"); cities[1][1] = new String("纽约"); 杨辉三角形,又称帕斯卡三角形,是二项式系数在三角形中的一种几何排列。程序5.8打印输出前10行杨辉三角形。 程序5.8Triangle.java package com.boda.xy; public class Triangle{ public static void main(String[] args){ int i, j; var level = 10; var triangle = new int[level][]; for(i = 0;i < triangle.length;i++){ triangle[i] = new int[i+1]; } // 为triangle数组的每个元素赋值 triangle [0][0] = 1; for(i = 1;i < triangle.length;i++){ triangle[i][0] = 1; for(j = 1; j < triangle[i].length-1; j++){ triangle[i][j] = triangle[i-1][j-1]+ triangle[i-1][j]; } triangle[i][triangle[i].length-1] = 1; } // 打印输出triangle数组的每个元素 for(i = 0;i < triangle.length; i++){ for(j = 0;j < triangle[i].length;j++) System.out.printf("%5d",triangle[i][j]); System.out.println(); // 换行 } } } 程序运行结果如下: 1 11 121 1331 14641 15101051 1615201561 172135352171 18285670562881 193684126126843691 提示Java也支持多维数组。例如,double [][][] sales = new double[3][3][4]; 语句声明并创建一个三维数组,共有36个元素。 5.7案例学习——打印输出魔方数 1. 问题描述 在中国传统文化中,有一种图表叫九宫图,它是在9个格子中放置数字1~9,使得每行、每列和每条对角线上的3个数字之和相等,如图57所示。对于该图,古人还给出了图中数字的记忆方法, 图57九宫格 “戴九履一,左三右七,二四为肩,六八为足,五居中央”。 数学上已经证明,对于任何n×n个自然数(1,2,…,n2 ,n为奇数)都有这样的排列,使得每行、每列和每条对角线上数的和都相等。这些数称为魔方数。编写程序,要求用户从键盘输入要打印的魔方数(比如5),程序计算并输出魔方数。 2. 设计思路 对于n为奇数的魔方数,可以按下列方法将1~n2的数填充到二维数组的元素中。首先把1放在第0行的中间,剩下的数2,3,…,n2依次向上移动一行,并向右移动一列。当可能越过数组边界时需要“绕回”到数组的另一端。例如,如果需要把下一个数放到-1行,就将其存储到n-1行(最后一行); 如果需要把下一个数放到第n列,就将其存储到第0列。如果某个特定的数组元素已被占用,就把该数存储在前一个数的正下方。 根据上述规则,可按下列步骤设计实现本案例。 (1) 首先从键盘输入一个正奇数size,然后定义一个size×size的二维数组,代码如下: int [][] magic = new int[size][size]; (2) 将1填充到数组的第一行中间元素中,代码如下: int row = 0, col = size/2; magic[row][col] = 1; // 将1填到第一行中间元素 (3) 将2到size×size填充到数组其他元素中,这里重点是确定下一个元素填充的位置。通过定义临时变量存放元素的行和列,如果下一个位置超出数组元素范围,将改变其行或列的值,最终确定正确位置。 (4) 输出填充了数值的二维数组的每个元素。 3. 代码实现 该案例的完整实现代码如程序5.9所示。 程序5.9MagicSquare.java package com.boda.xy; import java.util.Scanner; public class MagicSquare { public static void main(String[] args) { var input = new Scanner(System.in); int size; System.out.print("请输入魔方矩阵的大小(1-99):"); size = input.nextInt(); if(size < 0 && size % 2 == 0){ System.out.println("你输入的数不是奇数,不能计算魔方数!"); System.exit(0); } var magic = new int[size][size]; // 用1到size×size值填充每个元素 int row = 0, col = size/2; magic[row][col] = 1; // 将1填充到第一行中间元素 for(var n = 2; n <= size*size; n++){ int tempx,tempy; tempx = row; tempy = col; tempx = tempx-1; tempy = tempy+1; if(tempx < 0) tempx=size-1; if(tempy == size) tempy = 0; if(magic[tempx][tempy]!=0){ magic[row+1][col] = n; row++; }else if(magic[tempx][tempy]==0) { magic[tempx][tempy]=n; row = tempx; col = tempy; } } // 输出数组元素 for(var i = 0; i < magic.length; i ++){ for(var j = 0;j < magic.length; j ++){ System.out.printf("%4d",magic[i][j]); } System.out.println(); } } } 4. 运行结果 运行程序,当输入5时的运行结果如图58所示。 图58案例输出结果 关于魔方矩阵,本案例只计算输出奇数的矩阵。实际上,对于偶数的矩阵,也有一些满足这个特性,但有些不满足。读者可自己到网上查找资料,了解它的特性。 5.8小结 本章详细介绍了Java语言的数组及其应用,主要知识点包括数组的创建和使用,数组元素的访问、复制等,数组的查找和排序,java.util.Arrays类的使用,二维数组的创建和使用等。 下一章将介绍Java语言的字符串类,内容包括String类的基本操作,文本块,命令行参数,格式化输出以及StringBuilder类的操作。 编程练习 1. 给定数组a的声明如下,编写程序,计算所有元素的和、最大值、最小值及平均值。 double []a = {75, 53, 32, 12, 46, 199, 17, 54}; 2. 编写一个方法,求出一个double型数组中的最小元素,方法的声明格式如下: public static double min(double[] array) 编写测试程序,提示用户从键盘输入5个double型数,并存放到一个数组中,然后调用这个方法返回最小值。 3. 编程打印输出Fibonacci数列的前20个数。Fibonacci数列是第一个和第二个数都是1,以后每个数是前两个数之和,用公式表示为: f1=f2=1,fn=fn-1 + fn-2(n ≥ 3)。要求使用数组存储Fibonacci数。 4. 编写程序,定义一个有10个元素的整型数组,然后将其前5个元素与后5个元素对换,即: 第1个元素与第10个元素互换,第2个元素与第9个元素互换,……,第5个元素与第6个元素互换。分别输出数组原来各元素的值和互换后各元素的值。 5. 编写程序,定义一个有8个元素的整型数组,然后使用选择排序法对该数组按升序排序。选择排序法先找到数列中最小的数,然后将它和第一个元素交换。接下来,在剩下的数中找到最小数,将它和第二个元素交换,以此类推,直到数列中仅剩一个数为止。 6. 编写一个方法,计算给定的两个数组之和,格式如下: public static int[] sumArray(int[] a, int[] b) 要求返回的数组元素是两个参数数组对应元素之和,不对应的元素直接赋给相应的位置,如{1,2,4}+{2,4,6,8}={3,6,10,8}。 7. 编写一个方法,合并给定的两个数组,并以升序返回合并后的数组,格式如下: public static int[] arrayMerge(int[] a, int[] b) 例如,一个数组是{16,13,15,18},另一个数组是{29,36,100,9},返回的数组应该是{9,13,15,16,18,29,36,100}。 8. 编写程序,使用下面的方法头编写一个解一元二次方程式的方法: public static int solveQuadratic(double [] eqn, double[] roots) 一元二次方程式ax2+bx+c=0的系数都传给数组eqn,然后将两个非复数的根存在roots中。方法返回根的个数。 9. 编写程序,使用筛选法求出2~100中的所有素数。筛选法是在2~100的数中先去掉2的倍数,再去掉3的倍数,以此类推,最后剩下的数就是素数。注意: 2是最小的素数,不能去掉。 10. 如果两个数组list1和list2的长度相同,而且对于每个i,list1[i]都等于list2[i],那么认为list1和list2是完全相同的。使用下面的方法头编写一个方法,如果list1和list2完全相同,那么这个方法返回true。 public static boolean equals(int [] list1, int [] list2) 11. 编程求解约瑟夫(Josephus)问题: 有12个人排成一圈,从1号开始报数,凡是数到5的人就离开,然后继续报数。试问: 最后剩下的一人是谁? 12. 编写程序,从一副52张的牌中选出4张,然后计算它们的和,程序应该显示得到和为24的选牌次数。A、J、Q和K分别表示1、11、12和13。 13. 有下面两个矩阵A和B: A=135-36013-57-21925 B=0-1-27-16-613212-8-13 编写程序,计算: (1)A+B; (2)A-B; (3)矩阵A的转置。 14. 编写下面的方法,返回二维数组中最大元素的位置。 public static int[] locateLargest(double [][] a) 返回值是包含两个元素的一维数组。这两个元素表示二维数组中最大元素的行下标和列下标。编写一个测试程序,提示用户输入一个二维数组,然后显示这个数组中最大元素的位置。 请输入数组的行数和列数: 3 4 请输入每行元素的值: 23.535210 4.53453.5 35445.59.6 最大元素的位置是(1,2)。 15. 编写程序,从键盘输入矩阵阶数n,给n阶矩阵的元素按行序从1~n×n的顺序赋值,然后将矩阵按顺时针旋转90°,输出旋转后的矩阵。运行结果如下: 请输入矩阵的阶数:4 13951 141062 151173 161284