其他分享
首页 > 其他分享> > 数组

数组

作者:互联网

数组的简介

数组是一种数据结构,存储同一基本数据类型的数据、或具有相同父类/接口的引用的集合 。

基本类型数组的元素是同一基本数据类型,引用数组的元素可以是不同类/接口的引用,但这些类/接口必须继承同一个类/接口。

数组是一种线性表的结构,数组元素之间有相对次序,通过用一段连续的内存空间存储一组相同类型的数据、并用物理内存的连续性来表达元素之间的前后关系。

Java的数组有以下基本特点:

数据元素的访问是通过整数下标进行的,数组支持随机访问;

数组一旦创建,就不能再改变其大小,如果希望在动态扩展大小,那么应该使用另一种数据结构ArrayList;

数组本质上是一个对象,而既然是对象,就应该有成员。

下面,我针对这些基本特点进行讨论。

 

一、关于数组下标

(一)按数组下标随机访问

访问数组的任一元素是通过一个整型下标的方式,这点与C++类似。

与C++不同的是,Java会在运行时(编译时不会)做边界检查:如果创建一个100个元素的数组,当试图访问0-99以外的下标时,程序就会抛出IndexOutOfBoundsException异常而终止执行。C++则可以通过指针运算,访问到不属于数组的内存中的数值,这很可能引发一些致命的问题。数组边界检查是Java相对于C++的好处之一。

#include <stdio.h>
 
int main()
{
   int i = 0;
   int arr[3] = {0};
	
   for (; i<=3; i++) {
      arr[i] = 5;
      printf("hello world\n");
   } 
   
   return 0;
}

上面的例子会打印4个hello world,而第四次执行时,尽管arr数组没有arr[3]这个元素,但是编译、运行时却没有报告异常。实际开发中,就有可能出现上述程序正常运行但是其中有个致命的bug的情况,只不过还没有爆发严重问题而已。

(二)随机访问特性的实现

数组之所以可以随机访问,有两个原因:连续的内存空间+相同类型的数据。

我们知道,计算机会给每个内存单元分配一个地址,然后计算机通过地址访问内存中的数据。当计算机要随机访问数组的某个元素时,会通过如下的寻址公式,计算出该元素的内存地址:

address of a[i] = base_address + i * data_type_size

这也就是数组的内存模型。可以看到,连续的内存空间让计算机知道要找下一个元素往哪里走;相同类型的数据让计算机知道,步长为多少算一个元素。这,就是数组可以随机访问的原因。

(三)为什么数组下标不是从1开始,而是从0开始?

数组下标从1开始不是更符合人类的思维吗?关于这个问题,有2个可能原因。

其一,历史原因。我们知道,很多高级语言都是模仿C语言设计的,而C语言的数组就是从0开始,所以这些语言如Java就保留了从0开始的习惯。当然,有些语言则不是,像MATLAB就是从1开始的。而Python则支持负数的下标,Python的下标可以理解成偏移,其正数下标即从首元素正向偏移多少个元素,其负数下标即从尾元素逆向偏移多少个元素。

其二,从数组的内存模型来看。若用a表示数组a的首地址,则a[0]的地址为偏移为0的位置(即首地址),a[k]的地址为偏移为k个data_type_size内存的位置。所以,我们计算a[k]的内存地址时是这么计算的:

address of a[k] = base_address + k * data_type_size

 但如果下标是从1开始的话,则计算a[k]的内存地址是这么计算的:

address of a[k] = base_address + (k-1)  * data_type_size 

这么一来,每次随机访问数组元素就多了一次减法操作,对CPU而言就多了一次减法操作的指令。而数组作为非常基础的数据结构,经常用到随机访问的操作,所以效率方面应该尽可能做到极致,这大概就是下标从0开始的原因吧。

 

二、数组变量的声明&数组的创建&数组的初始化

 (一)数组变量的声明

type[]  array_name;

数组变量的声明方式如上所示,其中type可以是八种基本数据类型,也可以是Object、Collection、自定义类等引用类型。

(二)数组的创建

array_name  =  new type[20]; 

数组的创建,即数组的实例化的方式如上,如果只保留右边部分,则创建了一个匿名数组,匿名数组可以在调用方法时作为参数传递给方法。type的类型同样可以是基本数据类型,也可以是引用类型。

数组变量的声明&数组的创建可以写在一条语句上:

type[] array_name = new type[20];

(三)数组的初始化

当以上面的方式创建数组时,Java会默认对数组进行初始化,如果type是数值类型,则所有元素初始化为0;如果是布尔类型,则初始化为false;如果是引用类型,则初始化为null。

此外,还有另外一种创建数组的方式,如下所示,这种方式允许我们在创建数组的时候,顺带着进行指定初始化。

int[] intArray = new int[] {1, 2, 3, 4, 5};

创建的数组的长度取决于大括号里面元素的个数。我们可以去掉右边的new int[ ],简写为:

int[] intArray = {1, 2, 3, 4, 5};

 

四、数组的常规操作

(一)数组的循环遍历

循环遍历,就是通过循环访问数组的每一个元素。数组有两种遍历方式,一种是传统的for循环,一种是for-each循环。

传统的for循环

// traverse the array using for loop 
int[] intArray = {1, 2, 3, 4, 5};
for (int i=0; i<intArray.length; i++) {
	System.out.print(intArray[i] +" ");
}	
//输出:1 2 3 4 5

for-each循环

int[] intArray = {1, 2, 3, 4, 5};
for (int element : intArray) {
	System.out.print(element +" ");
}
//输出:1 2 3 4 5

for-each循环的语法格式为:

for (type var : array) {
        statements using var;         
}

这种方式是迭代array中的每一个元素,并将它们赋值给变量var,然后在内部的statements中应用这个var。

它等价于:

for (int i=0; i<array.length; i++) {
        type var = array[i];
        statements using var;  
}

for-each特性是Java5引入的,它使用的依然还是for关键字,常常用于遍历数组或者一个集合类,如ArrayList。 

for-each的好处是:可以依次处理数组中的每个uansu,并且不必为下标的起始值、终止值操心,所以,自然就不会担心运行时抛出IndexOutOfBoundsException异常了。

不过,for-each也有不足之处:

无法在遍历的时候对元素进行更新,这是因为它主要将数组元素赋值给var变量,在那之后无论你对var进行何种操作,都与元素无关;

无法在遍历中使用下标、返回下标,不能返回某个指定元素的下标;

无法指定遍历的步长,只能一个个遍历完所有元素;

无法从后往前遍历,只能固定地从前往后。

下面举个例子。

// the shortage of for-each loop
int[] intArray = {1, 2, 3, 4, 5};
for (int element : intArray) {
	element = 2 * element;
}
for (int element : intArray) {
	System.out.print(element +" ");
} 
//结果:1 2 3 4 5

所以说,无法在for-each遍历中更新数组元素。

这里对什么时候使用for循环,什么时候使用for-each循环做个小结:

如果只是需要遍历所有元素或者做一些简单的处理,那么可以用for-each;

如果只是遍历某个范围不希望遍历所有元素、遍历时需要更新元素、循环内部需要用到下标、需要从后往前遍历,那么应该使用传统的for循环

(二)打印数组信息

我们可以使用Arrays的toString()方法打印数组内容。

import java.util.Arrays;

public class ArrayTest {
	static class Student {
		public int id;
		public String name;
		public Student(int id, String name) {
			this.id = id;
			this.name = name;
		}
		
		@Override
		public String toString() {
			return this.id +" "+ this.name; 
		}
	}
	
	public static void main(String[] args) {
		// print the info of array
		int[] intArray = {1, 2, 3, 4, 5};
		System.out.println(Arrays.toString(intArray));
		Student[] stus = new Student[3];
		for (int i=0; i<stus.length; i++) {
			stus[i] = new Student(i+1, "stu_"+i);
		}
		System.out.println(Arrays.toString(stus));	
	}
}	

其结果如下:

如果数组是自定义类型,那么需要重写自定义类的toString()方法,否则打印的是各个元素的引用。

(三)数组的排序

可以使用Arrays的sort()方法对数组进行排序。

// sort the array
short[] shortArray = {2, 3, 1, 7, 4, 5, 9, 8, 6};
Arrays.sort(shortArray);
System.out.println(Arrays.toString(shortArray));
//结果:[1, 2, 3, 4, 5, 6, 7, 8, 9]

 

 

(四)数组元素的查找、更新

按值查找

 

按下表查找

 

更新

 

 

五、多维数组

Java中的多维数组有规则的,也有不规则的。其中规则的多维数组跟C++的多维数组一样,逻辑上是一个矩形,每一行拥有相同的列数;不规则的多维数组,则不同行可以有不同的列数,不规则的多维数组可以称为Jagged Arrays。

Java的多维数组本质上还是一维数组,只不过这个一维数组的元素是数组而已,这些数组元素可以有各自的length。

(一)规则多维数组的声明、创建、初始化、遍历

多维数组的声明和创建方式为:

int[][] intArray = new int[10][20];
int[][][] intArray = new int[10][20][30];

这种方式会将所有子数组的元素都默认初始化为0,强调一下,多维数组的元素是一个个数组,像上面的intArray二维数组中,intArray[1][1]不是它的元素,它的元素是10个子数组intArray[i],i=0,1,...,9,intArray[1][1]是子数组intArray[1]的第二个元素。

举个例子:

// multidimensional array 
int[][] intArray = new int[3][3];
for (int i=0; i<3; i++) {
	for (int j=0; j<3; j++) {
		intArray[i][j] = i * j;
	}
}
for (int i=0; i<3; i++) {
	for (int j=0; j<3; j++) {
		System.out.print(intArray[i][j] +" ");
	}
	System.out.println();
}

结果为:

 

(二)不规则多维数组的声明、创建、初始化、遍历

// multidimensional array
//Declaring and Creating
int[][] intArray = new int[2][];
intArray[0] = new int[3];
intArray[1] = new int[2];
//Initializing 
int count = 0;
for (int i=0; i<intArray.length; i++) {
	for (int j=0; j<intArray[i].length; j++) {
		intArray[i][j] = ++count;
	} 
}
//print info of array
for (int i=0; i<intArray.length; i++) {
	for (int j=0; j<intArray[i].length; j++) {
		System.out.print(intArray[i][j] +" ");
	}
	System.out.println();
}

 结果为:

当遍历不规则多维数组时,就不能使用固定的数字边界了,而应该使用数组的length属性。

(三)采用显式初始化的方式创建多维数组

// multidimensional array
int[][] intArray = {{2, 7, 9}, {3, 6}, {7, 4, 2}};
for (int i=0; i<intArray.length; i++) {
	for (int j=0; j<intArray[i].length; j++) {
		System.out.print(intArray[i][j] +" ");
	}
	System.out.println();
}
System.out.println("=======");
for (int[] element : intArray) {
	for (int item : element) {
		System.out.print(item +" ");
	}
	System.out.println();
}

结果为:

 

六、数组的应用

(一)数组的拷贝

(1)一维数组的深拷贝

// copy array	
int[] intArray = {1, 2, 3};
int[] cloneArray = intArray.clone();
System.out.println(intArray == cloneArray);
System.out.println("=======");
System.out.println(Arrays.equals(intArray, cloneArray));

结果为:

可见利用数组的clone方法返回的是一个新建的相对于原数组独立的数组。

另外,我们可以用Arrays的equals()方法判断两个数组的内容是否相同。

(2)多维数组的浅拷贝

// copy array 
int[][] intArray = {{1, 2, 3}, {4, 5}};
int[][] cloneArray = intArray.clone();
System.out.println(intArray == cloneArray);
System.out.println(intArray[0] == cloneArray[0]);
System.out.println(intArray[1] == cloneArray[1]);
System.out.println("=======");
System.out.println(Arrays.equals(intArray, cloneArray));
System.out.println(Arrays.deepEquals(intArray, cloneArray));
System.out.println("=======");
int[][] intArray2 = {{1, 2, 3}, {4, 5}};
System.out.println(Arrays.equals(intArray, intArray2));
System.out.println(Arrays.deepEquals(intArray, intArray2));

结果为: 

 

从结果中可以看出,尽管intArray和cloneArray是不相等的引用,但是它们的元素(子数组)却一一对应相等,所以说,多维数组的拷贝是浅拷贝。

由于intArray、cloneArray两个二维数组的元素是指向相同子数组,所以它们的元素是相同的引用值,这也是为什么Arrays.equals(intArray, cloneArray)返回的是true。而由于intArray2是不同于intArray的引用,所以,Arrays.equals(intArray, intArray2)返回的是false。

Arrays的deepEquals()方法可以用于比较多维数组的内容是否相同。尽管intArray2不等于intArray,但是它们的内容相同。

(二)在方法中返回数组

 

 

(三)传递数组给一个方法(作为方法的参数)

 

标签:数组,int,元素,System,Arrays,intArray
来源: https://www.cnblogs.com/JeremyChan/p/11149872.html