第4章 数组
在第2章中我们学习了变量,我们知道变量可以用于保存数据,但是一个变量只能保存一个值,这样存在很大的局限性。例如:要计算一个班50名同学的平均成绩,我们就只能定义50个变量,然后保存每个同学的成绩,然后累加求和并计算平均值。这样不仅效率低下,代码可读性也比较差,那么如何解决这个问题呢,就需要用到我们在本章节内学习的数组。
数组是编程语言中最常见的一种数据结构,可以存储多个数据,每个数组元素放数据,通常可以通过数组元素的索引来访问数组元素,包括为数组元素赋值和去除元素的值。那么,什么数组呢?
数组就是:具有相同数据类型且按照一定次序排列的一组数据的集合。
在数组的概念中我们需要注意以下几点:
-
具有相同的数据类型,也就是说存放在一个数组的元素必须数据类型相同,不能把不同类型的元素放进同一个数组。
-
按照一定次序排列,数组中的元素是有序的,这句话包含两层含义,第一:元素是有顺序的。也可以理解为每个数组元素都有一个编号,这个编号是连续的(我们通常把这个编号叫做数组的下标或者索引);第二:元素存放的顺序和取出的顺序是一致的,也就是说存放元素的顺序是10,20,30,40。那么取出的顺序也是10,20,30,40。
-
一组数据,一组数据是指元素的个数可以是0个,也可以是1个,也可以是多个,Java中允许数组中有0个元素,也就是我们所说的空数组。
4.1 定义数组
在第2章我们知道Java的数据类型可以分为两大类:基本类型和引用类型,其中基本类型有8个,引用类型则包括:对象,数组,接口。因此数组也是一种数据类型。首先我们学习数组的定义方式。数组的定义方式有两种语法:
type[] arrayName;
type arrayName[];
对于两种语法格式而言,通常推荐使用第一种方式,因为第一种方式语义明确且可读性强,对于type arrayName[]这种方式,很容易误解为定义了一个变量。前面指出数组也是一种数据类型,因此可以理解为type[] 是一种类型,注意,这种类型和type类型不同,例如:int类型是一个基本类型,而int[]则是一个引用类型。
另外,数组的初始化一旦完成,数组在内存中所占的空间将被固定下来,因此数组的长度不可改变,即使把数组元素全部清空,但是数组占用的空间仍然存在且属于该数组,数组的长度仍然不可变。
数组是一种引用类型的变量,当仅仅是定义了数组后,仅仅是定义了一个引用变量,这个变量还未指向任何有效内存,此时整个数组还不能使用,只有对数组初始化后才能使用。
4.1.1 数组的初始化
Java中数组必须先初始化,然后才可以使用。所谓初始化,就是为数组的元素分配内存空间,并为每个元素赋初始值。
数组的初始化有两种方式:
- 静态初始化:初始化时由程序员指定每个元素的初始值。由系统觉得数组的长度
- 动态初始化:初始化是程序员指定数组长度,由系统为元素分配初始值。
静态初始化语法格式如下:
arrayName = new type[]{e1,e2,e3}
或者
arrayName = {e1,e2,e3}
在上面的语法格式中,前面的type就是数组元素的数据类型,此处的type必须与定义数组变量时的type类型相同,并使用大括号把所有的组数元素括起来,多个元素之间用逗号隔开。
在第二种方式中,使用大括号来定义一个数组,大括号把所有的元素括起来形成一个数组。只有在定义数组的同时执行数组初始化才支持使用简化的静态初始化。
下面我们通过示例来定义并初始化数组
package cn.bytecollege;
/**
* 本例将演示定义及初始化数组
* @author MR.W
*
*/
public class DefineArray {
public static void main(String[] args) {
//定义了一个整型数组ary
int[] ary;
//定义了一个char类型数组chars
char[] chars = {'a','b','c'};
//初始化ary
ary = new int[]{1,2,3,4};
//此种写法编译器编译出错
// chars = {'a','b','c'};
}
}
在上例中我们定义了两个数组,分别是整型数组ary,和字符型数组chars,其中ary我们使用了静态初始化的完全形式,对其初始化,数组元素分别为1,2,3,4,因为有4个元素,所以ary长度为4,对于chars我们发现并能对其定义后使用简化初始化的形式赋值,编译器会给出错误提示,因此,如果要使用简化形式初始化数组,必须在数组定义好后就对其进行初始化,如12行代码所示。
需要注意的是,我们在使用静态初始化数组时,并不需要显示的指定数组长度。由编译器根据数组元素个数来确定数组长度,因此,下列写法是错误的。
int[] a = new int[3]{1,2,3}
4.2.2 动态初始化
动态初始化只指定数组的长度,由系统为每个元素指定初始值。动态初始化的语法格式如下:
arrayName = new type[length];
在上面的语法中,需要指定一个int类型的length参数,该参数指定了数组的长度,也就是可以容纳数组元素的个数,与静态初始化类型,此处的type必须与定义数组时使用的type类型相同。下面示例如何对数组进行动态的初始化。
int[] ary = new int[3];
char[] chars = new char[5];
执行动态初始化时,开发者只需指定数组的长度,即为每个元素指定所需的内存空间,系统将负责为这些数组元素分配初始值。指定初始值是,系统按如下规则分配初始值:
-
数组元素的类型是基本类型中的整型(byte,shor,int,long),则数组元素的初始值为0;
-
数组元素的类型是基本类型中的浮点型(float,double),则数组元素的值是0.0;
-
数组元素的类型是基本类型中的字符型(char),则数组元素的值是’\u0000’;
-
数组元素的类型是基本类型中的布尔型(boolean),则数组元素的默认值是false。
-
数组元素的类型是基本类型中的引用类型,则数组元素的默认值是null。
4.2 使用数组
数组中最常见的用法就是访问数组元素,其中包括对数组元素的取值和赋值,访问数组元素都是通过在数组引用变量后紧跟一个方括号[],方括号中是数组元素的索引值,这样就可以访问数组元素了,访问到数组元素后,就可以把一个数组当做普通变量使用了。
需要注意的是Java语言的数组索引是从0开始的,也就是说,第一个元素的索引值为0,后面的元素索引依次加1。
//定义一个长度为3的整型数组,并对其进行初始化
int[] ary = new int[]{1,2,3}
//读取第一个元素
System.out.print(ary[0]);
上面的代码演示了读取数组第一个元素。需要注意的是,如果范围元素时,指定的索引值小于0,或者大于等于数组的长度,程序编译时不会出现任何问题,但是运行时会出现异常:java.lang.ArraryIndexOutOfBoundsException(数组越界异常)。
同样的,如果我们需要对某个索引处的元素赋值或者重新赋值,也需要通过下标或者索引去访问,我们通过下面的示例来演示:
//定义一个整型数组,并对其动态初始化,此时数组中的元素都是默认值int[] a = new int [5];//对数组的第二个元素进行赋值a[1] = 10
所有的数组都提供了一个length属性,通过这个属性可以访问数组的长度,一旦获得了数组的长度,就可以通过循环来遍历数组的每个元素,下面我们通过示例来学习数组的变量
4.2.1 for循环遍历数组
package cn.bytecollege;/** * 本例将演示for循环遍历数组 * 以及数组的默认值 * @author MR.W * */public class ForArray { public static void main(String[] args) { //定义并动态初始化数组,此时数组元素全部是默认值 int[] a = new int[5]; for(int i = 0;i<a.length;i++) { System.out.println(a[i]); } }}
执行上面的程序我们发现打印了5个0,其中在for循环中a.length语句用户与获取数组的长度,a[i]则是读取数组的元素值,因此数组使用了动态初始化,数组的每个元素值都是由系统指定的默认值。下面我们演示为每个元素赋值。
package cn.bytecollege;/** * 本例将演示为元素赋值 * @author MR.W * */public class ForArray2 { public static void main(String[] args) { int[] a = new int[3]; for (int i = 0; i < a.length; i++) { a[i] = i+100; } for (int i = 0; i < a.length; i++) { System.out.println(a[i]); } }}
上例中我们演示对数组的元素通过索引进行赋值,其中第一个for循环通过索引为每个元素赋值,而第二个for循环同样通过索引为每个元素取值。
4.2.2 foreach遍历数组
Java5以后,Java提供了更简洁遍历数组的方式:foreach循环。使用foreach遍历数组和集合元素时,无需获得数组和集合长度,无需根据索引来访问数组元素。下面我们先来学习foreach语法:
for(type variableName:array){ //访问每个元素 }
上例的伪代码中type是数组元素的的类型,variableName是一个变量名,array是将要遍历的数组,foreach循环将自动将元素赋值给该变量。下面我们通过示例来遍历数组
package cn.bytecollege;/** * 本例将演示foreach遍历数组 * @author MR.W * */public class ForeachDemo { public static void main(String[] args) { //定义一个整型数组,并对其静态初始化 int[] a = {1,2,3,4,5}; //使用foreach遍历 for (int i : a) { System.out.println(i); } }}
上例中的foreach我们可以理解为增强for循环,不需要开发者根据索引去数组中取值,系统会自动从数组a中取到每一个元素,并赋值给i,我们只需要打印i即可,这种方式也有效的避免了数组下标越界异常。
4.3.深入数组
4.3.1 内存中的数组
数组引用变量只是一个引用,这个引用变量可以指向任何有效内存,只有当该引用执行有效内存后,才可以通过该数组变量访问数组。
int[] a;
此时只是定义了一个数组类型的变量,该变量没有指向任何有效内存,如果此时访问数组元素时,将会引发空指针异常。
与所有引用变量相同的是,引用变量是访问真实对象的根本方式,也就是说,如果希望在程序中访问数组对象本身,只能通过数组的引用变量来访问。
实际的数组对象被存储在堆内存中,如果引用该数组对象的数组引用变量是一个局部变量,那么它将被存储在栈内存中。(关于JVM的内容,我们将在Java高级中学习,此处简单了解即可)。数组在内存中的示意图如下图所示:
如果堆内存中数组不再被任何变量引用,这个数组将成为垃圾,该数组所占有的内存将会被jvm中的垃圾回收器回收。
4.3.2 数组长度不能被改变
package cn.bytecollege;/** * 本例将演示数组长度不可变 * @author MR.W * */public class ArrayLength { public static void main(String[] args) { int[] a = new int[4]; int[] b = new int[5]; a = b; System.out.println(a.length); }}
运行上例的代码,我们发现数组a的长度发生了变化,这就和我们数组长度一旦确定不可变的原则发生了冲突,那么这个过程中究竟发生了什么,我们通过图示来学习。
当代码执行完第10行时,此时数组a和数组b的内存示意图如下:
当代码执行第11行时,此时内存中发生如下变化:
从图示中可以看出,此时a并不执行原来的内存空间,而是和b一样指向了相同的内存空间,而原来a指向的内存还在内存中,也就是说变的仅仅是变量a指向的内存空间,而不是数组长度,a原来指向的数组长度还是4,并没有发生变化,因此数组一旦被初始化后长度不可变,如果发生了变化,一定是指向了新的数组。
4.3.3 没有多维数组
Java语言里提供了多维数组的支持,但是实际上并不存在多维数组,例如二维数组,我们完全可以认为是数组类型的数组:因为Java语言里的数组类型是引用类型,因此数组变量其实是一个引用,这个引用指向真实数组的内存。数组元素的类型也可以是引用,如果数组元素的引用再次指向真实的数组内存,此时就产生了多维数组,换句话说,多维数组本质都是一维数组。我们可以把数组当成一种数据类型,可以理解为数组类型的数组,二维数组的语法如下:
type[] [] a;
接着对这个数组进行初始化,语法格式如下:
type[] [] a = new type[length1][length2]
下面我们通过实例来定义二维数组,并对其进行遍历
package cn.bytecollege;import java.util.Arrays;import java.util.Iterator;/** * 本例将演示二维数组 * @author MR.W * */public class TwoDimensArray { public static void main(String[] args) { //此处可以理解为定义了一个3行4列的矩阵 //也可以理解为外层的数组长度为3,即包含了3个数组 //内存数组中每个数组有4个元素 int[] [] a = new int[3][4]; for (int i = 0; i < a.length; i++) { for (int j = 0; j < a[i].length; j++) { a[i][j] = i*j; } } System.out.println(Arrays.deepToString(a)); }}
通过上述代码我们可以理解为数组a是一个二维数组,也就是说数组a是长度为3的数组,而数组a中又存放了3个长度为4的数组,我们为每个元素赋值,deepToString则是Arrays工具类为我们提供了打印多维数组的方法。Arrays工具类会在下一小节讲解。
6. Arrays工具类
Java提供了Arrays工具类,里面包含了一些方法,可以直接操作数组。
-
int binarySearch(long[] a, long key):使用二分查找法查询key元素值在数组a中出现的索引,如果a数组不包含key元素,则返回负数,调用此方法时要求数组中的元素已经按升序排列。
-
T[] copyOf(T[] original, int newLength):该方法会把original数组复制成一个新数组,其中length是新数组的长度,如果length小于original数组的长度,则新数组就是原数组的前面length个元素,如果lenght大于original数组的长度,则新数组的前面元素就是原数组的所有元素,后面补充默认值,根据数组类型确定
-
copyOfRange(T[] original, int from, int to):这个方法与前面的类似,但是这个方法只复制原数组form索引到to索引的元素。
-
boolean equals(type[] a, tyte[] a2) :如果数组a 和a2长度相等,并且a和a2每个元素也相等则返回true,否则返回false。
-
void fill(long[] a, long val):该方法会把a数组的所有元素都赋值为val
-
void sort(type[] a):该方法对a数组进行排序
-
String toString(type[] a):该方法将一个数组转换成字符串,该方法按顺序吧多个元素连接在一起,元素之间用逗号隔开。
下面我们通过示例来学习上述方法:
示例1:查找元素
package cn.bytecollege;import java.util.Arrays;/** * 本例将演示数组查找元素 * @author MR.W * */public class ArraysSearch { public static void main(String[] args) { int[] a = {10,12,13}; //注意:使用此方法查找数组元素时,数组必须升序排列 int index1 = Arrays.binarySearch(a, 12); //结果为1,说明此元素索引为1 System.out.println(index1); int index2 = Arrays.binarySearch(a, 1); System.out.println(index2); }}
当调用该方法查找到目标元素后,即返回元素索引,如果没有找到则返回负值
示例2:赋值数组
package cn.bytecollege;import java.util.Arrays;/** * 本例将演示复制数组 * @author MR.W * */public class ArraysCopy { public static void main(String[] args) { int[] a = {5,7,8}; //如果length小于a数组长度,则只复制前n个元素 int[] b = Arrays.copyOf(a, 2); for (int i : b) { System.out.println(i); } //如果length大于数组a的长度,多余元素则以默认值填充 int[] c = Arrays.copyOf(a, 5); for (int i : c) { System.out.println(i); } }}
示例3:复制数组
package cn.bytecollege;import java.util.Arrays;/** * 本例将演示复制数组 * @author MR.W * */public class ArraysCopy2 { public static void main(String[] args) { int[] a = {2,5,3,9,6}; int[] b = Arrays.copyOfRange(a, 0, 2); for (int i : b) { System.out.println(i); } }}
从上例运行的结果来看,复制的元素时包含了form和to索引处的元素。需要注意的是,如果该方法的第三个参数超出了原数组长度,此时并不会抛出异常,会将原数组中所有的元素进行复制,空余的位置以默认值。
示例4:判断数组是否相等
使用此方法时需注意,两个数组相等不仅仅是长度相等,每个元素都相等才能算做两个数组相等。
package cn.bytecollege;import java.util.Arrays;/** * 本例将演示两个数组完全相等 * @author MR.W * */public class ArraysEquals { public static void main(String[] args) { int[] a = {1,2,3}; int[] b = {2,2}; //此时两个数组长度不相等,元素也不完全相等,因此打印false System.out.println(Arrays.equals(a, b)); }}
示例5:填充元素
package cn.bytecollege;import java.util.Arrays;/** * 本例将演示填充数组 * @author MR.W * */public class ArraysFill { public static void main(String[] args) { int[] a = new int[5]; //将元素所有的值填充为10 Arrays.fill(a, 10); for (int i : a) { System.out.println(i); } }}
示例6:数组排序
package cn.bytecollege;import java.util.Arrays;/** * 本例将演示数组排序 * @author MR.W * */public class ArraysSort { public static void main(String[] args) { int[] a = {3,6,1,8,0}; Arrays.sort(a); for (int i : a) { System.out.println(i); } }}
示例7:打印数组
package cn.bytecollege;import java.util.Arrays;/** * 本例将演示打印数组 * @author MR.W * */public class ArraysPrint { public static void main(String[] args) { int[] a = {2,3,4,5}; System.out.println(Arrays.toString(a)); }}
7.常见的排序算法
在实际开发和面试中我们经常会遇到排序的问题,在本小节内将列举出现频率比较高的几种排序算法:
7.1 冒泡排序
-
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
-
对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
-
针对所有的元素重复以上的步骤,除了最后一个。
-
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
public static void main(String[] args) { int[] a = {6,9,7,3,1}; for (int i = 0; i < a.length-1; i++) { for (int j = 0; j < a.length-i-1; j++) { if(a[j]>a[j+1]) { int temp =a[j]; a[j] = a[j+1]; a[j+1] =temp; } } } System.out.println(Arrays.toString(a));}
7.2 选择排序
-
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
-
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
-
重复第二步,直到所有元素均排序完毕。
public static void main(String[] args) { int[] a = {8,2,3,17,23,85,1,9}; for (int i = 0; i < a.length; i++) { int minIndex = i; for (int j = i+1; j < a.length; j++) { if(a[minIndex]>a[j]) { minIndex=j; } } if(minIndex!=i){ int temp = a[minIndex]; a[minIndex] = a[i]; a[i] = temp; System.out.println("每次结果>>>"+Arrays.toString(a)); } System.out.println("每轮结果==="+Arrays.toString(a)); } }
7.3 插入排序
- 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。
public static void main(String[] args) { int[] a = {87,86,82,10,30}; for (int i = 0; i < a.length-1; i++) { for (int j = i+1; j >0; j--) { if(a[j]<a[j-1]) { int temp = a[j]; a[j] = a[j-1]; a[j-1] = temp; } System.out.println("每次结果==="+Arrays.toString(a)); } System.out.println("第"+(i+1)+"轮结果>>>"+Arrays.toString(a)); } }
7.4 快速排序
- 分治法
- 挖坑法
public class QuickSort { /** * 双边循环 * @return */ public static int pivotIndex(int[]ary,int startIndex,int endIndex) { //保存左右两个指针,从第一个元素和最后一个元素开始 int left = startIndex; int right = endIndex; //获取基准,一般选择数组第一个数 int pivot = ary[startIndex]; //大循环,当左右两个指针不相等时,左指针右移,右指针左移 while (left!=right) { //右边指针向左移动,当对应元素大于基准数时继续移动,小于基准数时停止移动 while(right>left&&ary[right]>=pivot) { right--; } //左边指针向右移动,当小于基准数时继续移动,大于基准数时停止移动 while (right>left&&ary[left]<=pivot) { left++; } //此时如果左边指针小于右边指针时;左右指针交换元素 if(left<right) { int temp = ary[right]; ary[right] = ary[left]; ary[left] = temp; } } int index = left; //如果两个指针相等时,交换基准元素和当前位置的数 ary[startIndex] = ary[index]; ary[index] = pivot; return index; } public static void qucikSort(int[] ary,int startIndex,int endIndex) { if(startIndex>endIndex) { return; } int index = pivotIndex(ary,startIndex,endIndex); qucikSort(ary, startIndex, index-1); qucikSort(ary, index+1, endIndex); } public static void main(String[] args) { int[] a= {5,6,1,2,4,9,3}; qucikSort(a, 0, a.length-1); System.out.println(Arrays.toString(a)); }}/** * 单边循环 * @author MR.W */public class QucikSort2 { public static int pivotIndex(int[] ary,int startIndex,int endIndex) { int mark = startIndex; int pivot = ary[startIndex]; for (int i = startIndex+1; i <= endIndex; i++) { //如果指针指向的元素小于基准元素,干两件事情 //1.mark+1,扩大小于基准数的区间 //2.将指针所指向的数和mark位置处的数进行交换 if(ary[i]<pivot) { mark++; int temp = ary[i]; ary[i] = ary[mark]; ary[mark] = temp; } } //当循环结束时将mark位置的数和基准数进行交换 ary[startIndex]= ary[mark]; ary[mark] = pivot; return mark; } public static void quickSort(int[] ary,int startIndex,int endIndex) { if(startIndex>endIndex) { return; } int index = pivotIndex(ary, startIndex, endIndex); quickSort(ary, startIndex, index-1); quickSort(ary, index+1, endIndex); } public static void main(String[] args) { int [] a = {7,9,1,4,8}; quickSort(a, 0, a.length-1); System.out.println(Arrays.toString(a)); }}public static void main(String[] args) { int [] a = {5,1,6,2,3}; for (int i = 0; i < a.length; i++) { for (int j = 0; j < a.length-1; j++) { int temp; if(a[i]<a[j]) { temp = a[j]; a[j] = a[i]; a[i] = temp; } System.out.println("每次"+Arrays.toString(a)); } System.out.println("每轮"+Arrays.toString(a)); } System.out.println("最后"+Arrays.toString(a)); }