zoukankan      html  css  js  c++  java
  • 数组

    数组的简介

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

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

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

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

    数据元素的访问是通过整数下标进行的(支持随机访问),数组元素之间是有序的(下标从0开始);

    数组可以动态分配,数组变量的声明与数组的创建可以不在同一个位置进行;

    数组一旦创建,就不能再改变其大小(创建时指定的大小必须是int型,不能是short、long),如果希望在动态扩展大小,那么应该使用另一种数据结构ArrayList;

    数组可以是基本数据类型。可以是引用类型。如果是基本数据类型,则实际数值被存在一段连续的内存中。如果是引用类型,则连续内存存的只是一个个引用,实际对象被存在堆中。

    数组本质上是一个对象,其直接父类是Object。而既然是对象,就应该有成员,如我们可以通过length成员来获取数组的长度(这与C++不同,在C++中只能通过sizeof)。此外,每种数组类型都实现了Cloneable接口、java.io.Serializable接口。

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

    一、关于数组下标

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

    访问数组的任一元素是通过一个整型下标的方式,这点与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
    ");
       } 
       
       return 0;
    }

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

    char[] charArray = new char[3];
    System.out.println(charArray[-1]);

    结果抛出异常:

    Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1

    在Java中,当访问超过数组范围的索引时,则抛出IndexOutOfBoundsException异常,有助于我们修改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、自定义类等引用类型。type决定了哪种类型数据可以存放在数组中,如果存放不适当的数据,在运行期间会出现异常。注意,这里只是声明了一个数组变量,只是告诉编译器可以用它来指向一个存放type类型数据的数组,但是此时还没有创建数组。

    (二)数组的创建

    array_name  =  new type[size]; 

    数组的创建,即数组的实例化,指的是分配一段内存来组成一个数组。数组的创建方式如上,type指定了数组的内存单位,一个单位是一个元素,它同样可以是基本数据类型,也可以是引用类型。size指定了数组的元素个数或者说数组的长度。array_name将特定的引用指向数组,以后就可以通过array_name访问数组的内容。如果只保留等号右边部分,则创建了一个匿名数组,匿名数组可以在调用方法时作为参数传递给方法。

    可以看到,得到一个数组是一个两步过程。首先,声明一个数组变量指向特定的数组类型;然后,创建一个数组实例并将数组变量指向该实例。因此,Java中的数组是动态分配的。

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

    type[] array_name = new type[size];

    (三)数组的初始化

    当以上面的方式创建数组时,Java会默认对数组进行初始化,如果type是数值类型,则所有元素初始化为0;如果是布尔类型,则初始化为false;如果是char类型,则初始化为空格字符" ";如果是引用类型,则初始化为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的好处是:可以依次处理数组中的每个元素,并且不必为下标的起始值、终止值操心,所以,自然就不会担心运行时抛出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,但是它们的内容相同。

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

    像普通变量一样,我们可以传递数组给方法,如下面的例子所示。

    public class ArrayTest {
    	
    	public static void main(String[] args) {
    		// pass an array to method 
    		int[] intArray = {3, 1, 2, 5, 4};
    		sum(intArray);
    	}
    	
    	//get the sum of array values
    	public static void sum(int[] arr) {
    		int sum = 0;
    		
    		for (int i=0; i<arr.length; i++) {
    			sum += arr[i];
    		}
    		
    		System.out.println("sum of array values : "+ sum);
    	}
    }

    结果为:

    sum of array values : 15

    (三)在方法中返回数组

    同样也可以在方法中返回一个数组,如下面所示:

    import java.util.Arrays;
    
    public class ArrayTest {
    	
    	public static void main(String[] args) {
    	
    		// return an array from method 
    		int[] intArray = createArray();
    		System.out.println(Arrays.toString(intArray));
    		
    	}
    	
    	//create an array and return it to the caller
    	public static int[] createArray() {
    		return new int[] {1, 2, 3};
    	}
    }

    结果为:

    [1, 2, 3]

    七、数组的本质

    Java中数组本质上是一个对象。从前面也可以看到,数组有length属性,也有一些方法可以调用如clone()方法,所以说,数组具有对象的特点:封装了一些属性和方法,从这个角度来讲,数组就是对象。

    下面,我们验证一下。

    // an array is also an object
    int[] intArray = {1, 2, 3};
    System.out.println(intArray instanceof int[]);
    System.out.println(intArray instanceof Object);
    System.out.println(intArray.getClass());
    System.out.println(intArray.getClass().getSuperclass());

    结果为:

    可以看到,数组的确是对象,其直接父类是Object类。

    再来看看不同类型数组的对应类

    Object[] objArray = new Object[0];
    String[] strArray = new String[0];
    Integer[] integerArray = new Integer[0];
    byte[] byteArray = new byte[0];
    char[] charArray = new char[0];
    long[] longArray = new long[0];
    System.out.println(objArray.getClass());
    System.out.println(strArray.getClass());
    System.out.println(integerArray.getClass());
    System.out.println(byteArray.getClass());
    System.out.println(charArray.getClass());
    System.out.println(longArray.getClass());

     结果为:

    从结果可以知道,对于引用类型,它对应的类为“[L”后面加上完整的类名。

    Java数组的对象性与C++不同,C++中数组名只是一个指针,指向数组的首元素,没有属性也没有方法可以调用。当C++的方法调用数组时,除了传递数组名,还需要用sizeof传递数组长度,否则运行中很可能产生越界问题。相比之下,Java传递数组给方法就简单些,只需要传递数组名(数组的引用)即可,当方法内部需要获取数组的长度,直接调用数组的length属性就行了。

    数组对象的成员有:

    常数length,数组创建完就不再改变,可以是0、正整数;

    继承自直接父类Object类的所有成员(除了clone()方法),数组中覆盖掉了Object的clone()方法;

    公有的clone()方法,此方法是重写了Object的clone()方法。

    注意数组的equals()方法只是简单地继承Object的该方法,所以比较的是两个对象的引用是否相同,等同于用==判断两个对象是否是同一个对象。

    int[] intArray = {1, 2, 3};
    int[] intArray2 = {1, 2, 3};
    System.out.println(intArray == intArray2); System.out.println(intArray.equals(intArray2)); System.out.println(Arrays.equals(intArray, intArray2));

    结果为:

    false
    false
    true

     总结:

    1234

  • 相关阅读:
    min-max 容斥
    集训作业
    UOJ Test Round 3
    uoj Goodbye Dingyou
    Codeforces Round #516 (Div. 1) 题解
    Codeforces Round #517(Div. 1) 题解
    概率论(Ⅱ)
    Berlekamp-Massey算法学习笔记
    多项式取模优化线性递推总结
    [ZJOI2019]线段树
  • 原文地址:https://www.cnblogs.com/JeremyChan/p/11149872.html
Copyright © 2011-2022 走看看