在执行程序的过程中,经常需要存储大量的数据,例如,假设需要读取 100 个数,计算它们的平均值,然后找出有多少个数大于平均值。首先,程序读入这些数并且计算它们的平均值,然后将每个数与平均值进行比较判断它是否大于平均值。为了完成这个任务,必须将全部的数据存储到变量中。必须声明 100 个变量,并且重复书写 100 次几乎完全相同的代码。这样编写程序的方式似乎是不太现实的,那么该如何解决这个问题呢?
这就需要一个高效的有条理的方法,Java 和许多高级语言都提供了一种称作数组(array)的数据结构,可以用它来存储一个元素个数固定且元素类型相同的有序集。在现在这个问题中,可以将所有的100个数存储在一个数组中,并且通过一个一维数组变量访问它。那么什么是数组?
数组的介绍
数组——指的是一组(批量)数据,在内存中是若干个大小一致,类型一样,地址连续的存储空间,并提供角标访问元素,如果数组大小为10(即10个元素) ,则角标为:0~9,且在数组中,查找元素的时间复杂度O(1) 只要计算一次,即可得到相应的元素,与元素的个数是无关。
数组的定义方式:
1)数据类型[ ] 数组名=new 数据类型[长度]; //创建一个指定长度的数组,但是元素是默认值
如:创建一个长度为5的整型数组——int[] arr=new int[5];创建一个长度为7的double类型数组——double[] arr=new double[7]
2)数据类型[ ] 数组名=new 数据类型[]{元素1,元素2,....}; //创建一个数组,其长度为赋值的元素的个数
如
3)数据类型[ ] 数组名={元素1,元素2,....}; //与2)一样,只是不同的写法而已
我们说到数组的一堆数据,但是变量只有一个,那么如何查询数组中的元素以及数组是怎么创建以及加载的呢?是不是直接System.out.print(数组名)这样打印呢?我们先来看一下数组的内存机制,再来回答这个问题,如下图:
首先栈内存会加载main函数,假如在main函数里定义了一个名叫arr的数组,并定义其大小为5,但是并没有给其赋初值,此时堆内存就会加载出5个连续,大小一致,类型一样的空间,并且地址也是连续的。然后是将该空间的首元素的地址给了arr,这样就表示arr是这个数组。由此可以看出,数组的数据是存在“堆内存”中的,并且堆内存中的数组有一个性质就是:但凡实在堆内存中的数据 都有默认初始化值,具体的初始化值还要看数组的类型,比如int类型的数组的默认初始化为0.所以总结一下数组的创建流程为:
1.函数中定义变量空间并起名
2.在堆内存中根据长度创建数组
3.对每一个元素进行默认初始化 int->0 double->0.0 boolean->false
4.将数组中第一个元素的地址当成整个数组的地址传递给变量
5.变量指向该数组(引用数据类型)
因此上面的问题中,我们是不能直接用System.out.print(数组名)打印一个数组的。但是总是有特殊的。
对于 char[]类型的数组,可以使用一条打印语句打印如下:
char[ ] arr = {'J','A','V','A'};
System.out.print(arr);
数组的遍历
现在我们已经知道了怎么定义一个数组、怎么在数组中赋值,那么如何查询数组中的元素呢?前面说到数组是提供角标访问元素的,并且角标是从0开始的,数组长度为多少,最大的角标就是数组长度减1,Java中提供一种length的方法来得到数组的长度。其实只要用到一个for循环就可以访问所有存在数组中的元素。
class Demon01{
public static void main(String[] args){
bianli();
}
public static void bianli() {
int[] arr={1,2,3,4,5,6};
for(int i=0;i<arr.length;i++){ //从前往后遍历
System.out.print(arr[i]+" "); //逐个打印
}
System.out.println();
for(int i=arr.length-1;i>=0;i--){ //从后往前遍历
System.out.print(arr[i]+" "); //逐个打印
}
System.out.println();
}
}
数组的查找
那么相应的,查找数组中某个元素,就找其相对应的角标值就可以了,如果该元素不存在这个数组中,直接返回角标值为-1即可:
class Demon02{
public static void main(String[] args){
find();
}
public static void find(){
int[] arr={3,2,1,9,5,6,8,7};
int key=10; //找元素key的角标
int index=-1; //先不知道该元素存在,将角标置为-1
for(int i=0;i<arr.length;i++){ //从前往后或从后往前查找都是可以的
if(arr[i]==key){ //如果该元素在数组中存在
index=i; //将该元素的下标值返回
break; //直接结束循环,不在查找
}
}
System.out.println(index);
}
其中查找数组元素为线性查找,最好的情况下是第一个元素就查找到,时间复杂度为O(1),最坏的情况下是最后才找到,时间复杂度为O(n),取两者的平均情况,即线性查找的时间复杂度为O(n)
数组的扩/减容
在前面有提到过数组一旦被创建后其大小是固定的,那么在不浪费内存空间资源的情况下我们如何对数组的大小进行改变呢?我们先来看图解:
要想将数组arr扩容成arr1这样的数组或者缩容成arr2这样的数组,我们首先要把arr里面的元素都赋给arr1,然后将arr1的首元素地址指向arr,这样就达到了扩容的目的;缩容与之相反,只是元素会丢失。现在有一个问题,将原来的arr数组首元素地址不指向函数中变量arr,那么堆内存中arr的空间是一直就留在里面了吗?显然不是的,Java中有一个垃圾回收器,只要数组对象已经没有任何变量引用,则该对象为垃圾会被回收。这样我们就达到了改变对数组大小的目的。现在来看代码的实现:
class Demon03{
public static void main(String[] args){
resize();
}
public static void resize(){
int[] arr=new int[]{1,2,3,4,5};
int deltSize=6;//deltSize的取值取决于是对数组的扩还是缩 -缩 +扩
int[] newArr=new int[arr.length+deltSize]; //先定义一个新的我们需要长度的数组
//将原先的数据放入到新数组中
for(int i=0;i<Math.min(arr.length,newArr.length);i++){
newArr[i]=arr[i];
}
arr=newArr; //将新数组的首元素地址赋给原来的数组变量
for(int i=0;i<arr.length;i++){
System.out.print(arr[i]+" ");
}
}
}
数组的排序
我们知道数组的角标是成升序的,但是每个角标中存储的相应元素是无序的,那么就要对数组元素进行排序,依然依靠的是其角标,我们在排序时,是对数组的元素进行排序,对角标是不能进行排序的,现在先介绍三种排序方式。
- 选择排序
如果按升序排序的话,首先从第一个元素开始,将第一个元素与后面所有的元素对比,只要找到比当前元素小的元素,就将两个元素进行交换,直到找完为止,所以第一轮排序完以后,第一个元素为最小元素,一次类推,直到最后一个元素停止。所以每个元素都会与其他元素相比较一次,其时间复杂度为O(n²)
class Demon04{
public static void main(String[] args){
selectSort();
}
public static void selectedSort(){
int[] arr={5,1,3,2,7,4,9,8,6}; //待排序的数组
for(int i=0;i<arr.length-1;i++){ //从第一个元素开始
for(int j=i+1;j<arr.length;j++){ //与该元素后面的所有元素相比较
if(arr[i]>arr[j]){ //一旦有比该元素小的元素,将两个元素的位置进行交换
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
//继续向后查找
}
//直到每个位置的元素都查找完
}
}
for(int i=0;i<arr.length;i++){
System.out.print(arr[i]+" ");
}
}
}
- 冒泡排序
依旧按升序排序,冒泡排序是相邻两个元素之间比,如果前面的元素大于后面的元素,则交换两个元素,每结束一轮,最大的数就会放在最后,所以每进行一轮比较,查找的长度要减一,总共对比数组的大小次,所以其时间复杂度也为O(n²)
class Demon05{
public static void main(String[] args){
bubbleSort();
}
public static void bubbleSort(){
int[] arr={5,1,3,2,7,4,9,8,6};//待排序数组
for(int i=0;i<arr.length-1;i++){ //i表示比较的轮数
for(int j=0;j<arr.length-i-1;j++){ //每轮对比后最大的数就会放在最后,所以不用与最后面的数再进行比较
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]+" ");
}
}
}
- 插入排序(也叫希尔排序)
插入排序现将第i个数取出来,然后将该元素与前面的元素对比,如果前面元素大于该元素,则将前面的元素向后覆盖一位,直到查找小于该元素的位置,在小于该元素位置的后方放置该元素,所以每个元素并不会与所有其他的元素都相比较,虽然插入排序的时间复杂度也为O(n²),但是相对比前两个排序,还是插入排序相对较好。
class Demon06{
public static void main(String[] args){
insertSort();
}
public static void insertSort(){
int[] arr={5,1,3,2,7,4,9,8,6}; //待排序数组
for(int i=1;i<arr.length;i++){ //从第一个元素开始排序
int e=arr[i]; //将第i个元素拿出来
int j=i-1;
while(j>=0&&arr[j]>e){ //如果第i个元素前面有数且小于前面的这个元素
arr[j+1]=arr[j]; //然后将前面的元素都往后移一位,直到该元素是小于它后面元素的位置
j--;
}
arr[j+1]=e; //把元素放入指定位置
}
for(int i=0;i<arr.length;i++){
System.out.print(arr[i]+" ");
}
System.out.println();
}
}