一、数组的定义
数组是同一种类型数据的集合.也就是说数组就是一个容器,它的好处是可以自动的为数组中的每个元素从0开始编号,方便对这些元素的操作.
定义数组的格式有以下两种不同的形式:
格式1:
元素类型[] 数组名 = new 元素类型[元素类型或数组长度]; e.g. int[] arr = new int[5];
格式2:
元素类型[] 数组名 = new 元素类型[]{元素,元素,......}; e.g. int[] arr = new int[]{3,5,7,9} int[] arr = {3,5,1,7};
注意:对于数组的定义,一但建立,就一定要明确其长度.
二、数组的内存分配及特点
那么我们就来看看内存的划分,可以划分为以下五种:
1.寄存器
2.本地方法区//我们暂时设计不到,以后有机会了我们学习,主要运行的时本地系统平台中的内容
3.方法区
4.栈内存
5.堆内存
我们在这里主要学习一下栈内存和堆内存,其他的几种以后再学习吧.
栈内存:
存储的都是局部变量.方法中定义的变量都是局部变量,
栈内存处理数据的特点:
变量所属作用域一旦结束,该变量就自动释放.
堆内存:
存储的是数组和对象(其实数组就是对象).凡是new建立的,都在堆中,这也就是说我们数组就存放在堆内存中.
再说说堆内存处理数据的特点:
1.每一个实体都有一个首地址值
2.堆内存中的每一个变量都有默认初始化值,根据类型的不同而不同.整数是0,小数0.0或者0.0f,boolean是false,char是'\u0000'
3.垃圾回收机制//自动回收垃圾
三、数组操作中常见问题
我们看一下在数组操作中经常出现的两个小现象.
class ArrayDemo2 //数组-常见的问题 { public static void main(String[] args) { int[] arr =new int[3]; //System.out.println(arr[3]);//编译的时候不报错(ArrayIndexOutOfBoundsException),运行时才报错 //当访问到数组中不存在的角标时,就会发生异常. //arr = null; //System.out.println(arr[0]);//NullPointerException //当引用型变量没有任何实体指向时,还在用其操作实体,就会出现该异常. System.out.println(arr);// [I@139a55 @是分隔符,之前是实体的类型,[表示数组,I表示整型 } }
从上面的代码我们可以看出来我们在对数组的操作时会经常遇到ArrayIndexOutOfBoundsException和NullPointerException这两个异常.
当访问到数组中不存在的角标时,就会出现ArrayIndexOutOfBoundsException异常.
当引用型变量没有任何实体指向时,还在用其操作实体,就会出现NullPointerException异常.
最后就说一下最后一句语句的意义:当我们不知道程序中的一个实体是什么类型时,我们可以直接打印该实体,然后看打印出的字段的@之前的内容,就可以确定实体是什么类型了,例子中[表示的就是数组,I表示的数组的元素是整型.
四、数组常用操作
数组的遍历
当然,我们要操作数组中的数据,我们首先要做的第一个动作就是找数据,也就是遍历,这是数组操作中最常见的操作之一。下面我们看一个遍历的小例子:
class ArrayDemo3 //数组的遍历 { public static void main(String[] args) { int[] arr = {89,34,54,78};//静态初始化方式 System.out.println(arr[2]); for(int i=0;i<arr.length;i++)//length是数组的一个属性,长度 { System.out.print("arr["+i+"]="+arr[i]+"\t"); } } }
我们可以看到数组的遍历操作的核心思想就是对数组索引的操作,通过索引访问数组中的每一个元素。
数组的最值
说到数组,我们知道它里面存储了相当类型的多个数据,那么我们会经常用到找出该数组中的最值的问题,下面我们看一下数组最值的问题:
我们这里以获取最大值为例子:
class ArrayDemo4 //数组的最值 { public static void main(String[] args) { int[] arr = new int[]{32,54,65,78,93}; System.out.println("第"+(getMaxIndex(arr)+1)+"个元素"+getMaxElement(arr)+"是最大元素"); } /* 思路: 1.需要进行比较,并定义变量记录每次比较后较大的值 2.对数组中的元素进行遍历,和变量中记录的元素进行比较 如果遍历到的元素大于变量的值,就把该元素的值符给变量 3.遍历结束,变量记录的就是最大值 */ //这个方法用来获取一个数组元素中的最大值 public static int getMaxElement(int[] arr) { int maxElement = arr[0];//用来记录遍历过的元素中的最大数 for(int i=1;i<arr.length;i++) { if(arr[i] > maxElement) { maxElement = arr[i]; } } return maxElement; } //这个方法获取一个数组元素中的最大元素的角标 public static int getMaxIndex(int[] arr) { int maxIndex = 0;//用来记录遍历过的元素中的最大数的角标 for(int i=1;i<arr.length;i++) { if(arr[i] > arr[maxIndex]) { maxIndex = i; } } return maxIndex; } }
从上面的代码我们可以看出我在获取一数组的最大值时采用了两个不同的方法,一种直接获取数组元素中的最大值,一个方法获取了数组元素中最大元素的索引,这两个方法最终的目标都是获取了数组中的最大元素。
数组的排序
说到数组,我们又不得不说是就是排序,这个是我们以后实践中最容易用到的。
说到排序,我们知道排序的算法有很多,这里我们只看选择排序和冒泡排序两种排序方法:
1.选择排序:就是我们先选择一个索引为i的位置,用来存放索引为i~最后一个元素之间的最小元素
public static void selectSort(int[] arr) { for(int i=0;i<arr.length-1;i++)//先选择一个位置,进行遍历的次数,对于最后一个元素,不需要再遍历,所以i<arr.length-1 { for(int j=i+1;j<arr.length;j++)//第i轮遍历的元素个数,把最小的元素放到arr[i]中 { if(arr[j]<arr[i])//当后面的元素小于前面的元素,两个元素交换 { //int temp = arr[i]; //arr[i] = arr[j]; //arr[j] = temp; swap(arr,i,j); } } } }
这就是一个选择排序的算法,通过对上面方法的分析可以看出,每一次内循环比较满足条件时都会对元素进行一次转换,这样转换次数非常频繁。我们可以对上面的方法进行优化,通过减少元素的转换次数这个途径。
优化后的代码:
public static void selectSort_2(int[] arr) { for(int i=0;i<arr.length-1;i++)//先选择一个位置,进行遍历的次数,对于最后一个元素,不需要再遍历,所以i<arr.length-1 { int num = arr[i];//存放对应轮数的最小值 int index = i;//记录这个最小值的索引 for(int j=i+1;j<arr.length;j++)//第i轮遍历的元素个数,把最小的元素放到arr[i]中 { if(arr[j]<num)//当后面的元素小于前面的元素,则用num记录较小的内容,用index记录较小元素的索引 { num = arr[j]; index = j; } } if(index != i)//如果index的值不是i,就把这两个位置的元素进行置换,如果相等,就说明i对应的元素就是该轮的最小元素 { swap(arr,i,index); } } }
在上面的代码中我们通过定义两个变量,一个用来存放最小元素,一个用来存放最小元素对应的索引,这样我们可以通过记录最小元素索引的方式,记住最小元素,然后当每一轮结束之后把最小的元素放到对应的数组位置中(位置置换),这样就每一轮我们最多就只进行一次位置的置换。
2.冒泡排序:通俗的说冒泡排序就是像气泡一样把最大的元素移动到相应的位置。实际算法对于的第i轮,把0~i之间的元素通过相邻元素依次比较,把最大的元素移动到索引为i的位置。
//冒泡排序 public static void bubbleSort(int[] arr) { for(int i=0;i<arr.length-1;i++)//遍历的次数 { for (int j=0; j<arr.length-i-1; j++)//-x:为了让外循环增加一次,内循环参数与比较的元素个数递减;-1是为了避免角标越界. { if(arr[j]>arr[j+1])//相邻元素进行比较,把较大的元素向上移动 { //int temp = arr[j]; //arr[j] = arr[j+1]; //arr[j+1] = temp; swap(arr,j,j+1); } } } }
下面我们对上面的两种排序方法进行一下测试,测试之前我们为了提高代码复用性,对元素位置置换和数组打印两个功能进行了独立封装。
//位置置换 public static void swap(int[] arr,int a,int b)//这个要注意参数的确定,这里是数组的两个元素在置换 { int temp = arr[a]; arr[a] = arr[b]; arr[b] = temp; }
//数组的打印,我们专门把数组的打印独立封闭起来 public static void printArray(int[] arr) { System.out.print("["); for(int i=0;i<arr.length;i++) { if(i!=arr.length-1)//判断取掉最后一个','号 System.out.print(arr[i]+","); else System.out.println(arr[i]+"]"); } }
下面看测试代码和结果
class ArrayDemo5 { public static void main(String[] args) { int[] arr = new int[]{23,9,54,41,64,35,7,22}; int[] arr2 = new int[]{43,63,12,41,136,8,55,108}; printArray(arr); //selectSort(arr); selectSort_2(arr); printArray(arr); printArray(arr2); bubbleSort(arr2); printArray(arr2); } }
数组的查找
这个功能我们以后会经常用到,这里我们先看一个对普通数组的查找方法
/*
数组常见功能:查找.
*/
public static int getIndex(int[] arr,int x)
{
for(int i=0;i<arr.length;i++)//从第一个元素开始找
{
if(arr[i] == x)//如果找到对应元素则返回当前元素索引
{
return i;
}
}
return -1;//如果没有找到该元素则返回索引值-1
}
上面的方法是一个普通的查找方法,因为我们在查找过程中可能对所有的元素都找一遍。
下面我们就看一个非常有名的查找算法,那就折半查找,也叫做二分查找。当然这个方法有一前提,那就是该数组必须为有序数组。
public static int binarySearch(int[] arr,int key)
{
int min,mid,max;//定义三个变量做为三个指针
min = 0;//min代表最左边的指针
mid = (min + max) / 2;//mid代表中间的指针
max = arr.length-1;//max代表最右边的指针
while(arr[mid] != key)//当mid所指向的元素不等于key时,继续查找,否则则终止循环
{
if(key > arr[mid])//如果mid指向的元素小于key,则让最左边的min指针指向mid+1的位置
min = mid + 1;
else if(key < arr[mid])//反之则让最右边的max指针指向mid-1位置
max = mid - 1;
if(max < min)//如果出现max指针小于min指针时,说明没有找到key对应的元素,则返回-1
return -1;
mid = (min + max) / 2;//对中间的指针重新指向左右两个指针的中点
}
return mid;//当跳出循环说明找到了
}
再看另一个相同的的方法
public static int binarySearch_2(int[] arr,int key)
{
int min,mid,max;
int min = 0;
int max = arr.length-1;
while(min <= max)//当min>max则终止循环,返回-1
{
mid = (min + max) >> 1;//在循环内找出min指针和max指针的中点元素,>>1等同于除以2
if(key > arr[mid])//如果mid指向的元素小于key,则让最左边的min指针指向mid+1的位置
min = mid + 1;
else if(key < arr[mid])//反之则让最右边的max指针指向mid-1位置
max = mid - 1;
else
return mid;//否则就是key = arr[mid],那当然就是找到了
}
return -1;
}
我们测试一下
import java.util.*;
class ArrayDemo6
{
public static void main(String[] args)
{
int[] arr = new int[]{43,56,98,2,5,36};//无序数组
int x = getIndex(arr,3);
System.out.println("x="+x);
int[] arr2 = new int[]{3,9,12,19,23,45};//有序数组
int index = binarySearch(arr2,15);
System.out.println("index="+index);
int index1 = binarySearch_2(arr2,19);
System.out.println("index1="+index1);
//真实开发中,我们运用Arrays类中的binarySearch(Object[] a,Object key)方法
int index2 = Arrays.binarySearch(arr2,45);//如果存在,返回具体的角标位置
System.out.println("index2="+index2);
int index3 = Arrays.binarySearch(arr2,21);//如果不存在,返回的就是这个数的插入点(return -插入点-1)
System.out.println("index3="+index3);
}
}
我们看到Arrays类中的binarySearch(Object[] a,Object key)方法实现了数组的二分查找,并且当查找不元素不存在时,返回值的其实指明了该key可以插入数组并且数组仍然有序的插入点。我们在实际开发中直接用这个方法就可以了。
五、二维数组
二维数组,我们通俗点可以说成是数组中的数组,就是说我们可以把一个二维数组理解为一个元素为一维数组的一维数组。
二维数组的定义格式以下两种格式:
格式一:
int[][] arr = new int[3][2];
//定义了一个名为arr的二维数组,该二维数组中有3个一维数组,每一个一维数组中有2个元素
格式二:
int[][] arr = new int[3][];
//这个没有明确一维数组的长度
我们看看这两种格式的不同之处,先看一段测试代码:
class Array2Demo
{
public static void main(String[] args)
{
int[][] arr = new int[3][2];//定义一个名为arr的二维数组
System.out.println(arr);//直接打印二维数组 结果[[I@139a55,这是二维数组的地址
System.out.println(arr[0]);//直接打印二维数组中的索引0的一维数组 结果[I@1db9742,这是二维数组中索引0的一维数组的地址
System.out.println(arr[0][0]);//直接打印二维数组中索引0的一维数组索引0的元素 结果0,元素默认初始化为0
int[][] arr2 = new int[3][];//没有明确一维数组的长度
//分别对二维数组中的每一个一维数组进行初始化
//arr2[0] = new int[2];
//arr2[1] = new int[1];
//arr2[2] = new int[3];
System.out.println(arr2);//直接打印二维数组 结果[[I@106d69c,这是二维数组的地址
System.out.println(arr2[0]);//直接打印二维数组中的索引0的一维数组 结果null,这是二维数组中索引0的一维数组没有自动创建
//System.out.println(arr2[0][0]);//直接打印二维数组中索引0的一维数组索引0的元素 结果 NullPointerExcption
int[][] arr3 = new int[3][2];
System.out.println(arr3.length);//打印二维数组的长度,其实就是一维数组的个数.
System.out.println(arr3[1].length);//打印二维数组中索引为1的一维数组的长度.
}
}
从结果我们可以看两种格式定义的二维数组打印数组会返回的都是数组的地址,从而我们进一步分析内在存储为:
对于第一种格式,当运行时,先把arr变量存入栈中,然后在堆内存中为二维数组(实体)创建空间,并把数组的地址存入arr变量中,然后对每二维数组的每一个一维数组在堆内存中创建不同的空间,再分别把一维数组的地址存入一维数组中,最后对每一个一维数组的每一个元素进行默认初始化为0.
而对于第二种格式来说,对二维数组中的每一个一维数组都要进行初始化,当然对于这些数组初始化的大小可以不一样,也就是第二种格式比第一种格式相对来说要灵活。如果我们不对一维数组进行初始化,就会出现上面代码中运行的结果,打印二维数组的元素结果为null,打印一维数组的元素就会抛出NullPointerExcption异常。
这里我们知道第二种格式要比第一种格式定义的二维数组更灵活和实用。
当我们对一个二维数组遍历的时候,我们很自然想到的就是前面学习for语句时的一种现象,那就大圈套小圈,诚然,对于二维数组的很多操作,我们最常用的也就是这个原理,循环的嵌套,下面我们看下学习过程中编写的遍历方法和测试结果:
class Array2Demo { public static void main(String[] args) { int[][] arr4 = {{3,1,4},{2,5,8},{4,3,9}}; //如何遍历该二维数组? //我们独立写一个遍历的方法吧 printArray2(arr4);//结果:{{3,1,4},{2,5,8},{4,3,9}} } //这个方法用来遍历一个二维数组 public static void printArray2(int[][] arr) { System.out.print("{"); for(int i=0;i<arr.length;i++)//很明显就是大圈套小圈的原理 { System.out.print("{"); for(int j=0;j<arr[i].length;j++) { System.out.print(arr[i][j]); if(j<arr[i].length-1)//判断去除最后一个逗号 { System.out.print(","); } } System.out.print("}"); if(i<arr.length-1)//判断去除最后一个逗号 { System.out.print(","); } } System.out.print("}"); } }
结果: