在Java中,多维数组也是非常常用的数据结构,下面以二维数组为例,介绍二维数组的声明、创建、初始化和引用。
一维数组使用单层for循环就可以遍历,二维数组的遍历需要使用双层嵌套for循环。实例:513运行结果:1行1列的元素为:11行2列的元素为:21行3列的元素为:72行1列的元素为:32行2列的元素为:43行1列的元素为:53行2列的元素为:6
多维数组可以看作是一维数组的扩展。所以,它的声明就是在之前学习的数组,也叫一维数组,之后继续添加大括号,如果添加一组大括号,那么称之为二维数组,以此类推,就构成了 N 维数组。int intArray[A1][A2][A3][...][An]int intArray[2][2]; // 二维数组float floArtay[3][4][5]; // 三维数组与一维数组中的声明类似,在多维数组中,第如果直接进行定义初始化,那么一个维数可以不指定,但是后面的维数是要明确指定的。例如:int intArray[][5][3]
数组在编程语言中是非常重要的数据结构。本小节我们来一起学习 Java 数组,通过本小节的学习,你将了解到数组的基本概念,如何声明数组以及数组的声明有哪些方式,如何初始化数组以及数组初始化有哪些方式,数组的常用操作有哪些,多维数组的声明、创建、初始化以及迭代等等。多维数组部分将以二维数组进行为例讲解,理解了二维数组,再去理解多维数组就相对容易了。
对于二维数组,在某些特殊情况下,可以通过连续切片的方式进行访问。案例例如,我们创建一个连续整数组成的方阵:arr_2d = np.arange(16).reshape(4,4)Out: array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11], [12, 13, 14, 15]])对 arr_2d 构造一个连续切片:arr_2d[0][1:3]Out: array([1, 2])对多维数组的索引,想要达到同样的的效果,可以一次传入多个切片。例如对上述结果,可以修改为:arr_2d[0, 1:3]Out: array([1, 2])在上述步骤中,传入了 2 个切片。严格来讲,第一个切片是整数索引,是对数组的最外层(axis=0)进行选择;第二个切片是对数组的内一层(axis=1)进行选择。更一般地,我们可以自由地根据需求,构造想要的切片效果。例如:arr_2d[0:2, 1:3]Out: array([[1, 2], [5, 6]])上述案例在 axis=0 方向上选择了第 0 和第 1 行,在 axis=1 方向上选择了第 1 列和第 2 列,两种切片方向的聚焦部分即为切片索引的结果。需要指出的是,如果切片只有冒号,表示选取该方向的整个轴。例如,利用该方法,可以对二维数组进行列方向的切片:arr_2d[:, 1:3]Out: array([[ 1, 2], [ 5, 6], [ 9, 10], [13, 14]])上述案例实现了选择第一列和第二列的效果。
这里我们会把前面学习到的一维数组和多维数组进行一次练习。对于一维数组,我们将计算数组中所有整数的和。二维数组的例子会稍微复杂一点,我们交换一个有 N x N 个元素二维数组对角元素。
对于使用一维整型数组作为索引,如果目标是一维数组,那么索引的结果就是对应位置的元素;如果目标是二维数组,那么就是对应下标的行。案例对于上述案例中的names数组,通过传入一个特定顺序的整数列表(或ndarray),来按照指定顺序选取元素:names[[4,3,2,1]]out: array(['Jeremy', 'Jeremy', 'Ben', 'Tom'], dtype='<U6')如果使用负数,则会从末尾开始选取:names[[-1,-2,-3,-4]]out: array(['Ben', 'Tom', 'Jeremy', 'Jeremy'], dtype='<U6')案例同样地,对data二维数组,通过传入特定顺序的整数列表,来按照指定顺序选取行:data[[3,1]]out: array([[-0.76501214, 2.01128245, 1.0350961 , 0.81014769], [ 0.03461754, -0.91921724, -1.51730244, 0.68583205]])
C 语言中的多维数组与我们在数学中学习到的多维数组是一致的。如果你还不知道什么是多维数组也没有关系。多维数组可以看成是之前学习过的数组的扩展。它能让你完成一些仅仅依靠一维数组没有办法完成的事情。让很多事情完成的更为简单。
多维数组的赋值与取值与之前的一维数组是一样的。通过索引位置直接获取相应的数值或者进行赋值操作。例如 a[2][3] = 12 。由于有多维的存在。我们想要获取其中的每一个数值的时候,就要使用循环的嵌套。一般情况下,有几个维度,就要使用几重的嵌套。
我们先初始化一个有 N x N 个元素的二维矩阵,然后通过嵌套的循环语句,来访问数组中的元素,并且将对行列上的元素进行互换。如同下面表格中的所示。原始数组数组索引0123012341567829101112313141516变化后数组数组索引0123015913126101423711153481216请大家注意这个交换是对称的,也就是行列的位置发生了互换。#include <stdio.h>int main(){ short x = 0; short a[4][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}, {13, 14, 15, 16}}; printf("原始数组\n"); for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { printf("%d\t", a[i][j]); } printf("\n"); } for (int i = 0; i < 4; i++) { for (int j = i; j < 4; j++) // 请注意此处是位置交换的关键,我们每次都会缩小交换的范围。 { x = a[i][j]; a[i][j] = a[j][i]; a[j][i] = x; } } printf("交换位置后的数组\n"); for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { printf("%d\t", a[i][j]); } printf("\n"); } return 0;}运行结果:原始数组1 2 3 45 6 7 89 10 11 1213 14 15 16交换位置后的数组1 5 9 132 6 10 143 7 11 154 8 12 16
多维数组和一维数组一样,可以将全部的值一次完全赋给数组,也可以只初始化部分内容。你可以如同之前的一维数组一样,在大括号中以此写入初始化的数值。但是出于可读性的考虑,你最好按照数组的维数,用大括号对应。就如同下面的例子中一样。short a[2][2] = { 1, 2, 3, 4};short b[2][2] = { {1, 2}, {3, 4}};short c[2][2] = { 3, 4};short d[2][2] = { {}, {3, 4}};上面这些变量初始化中,数组 a 的初始化方式和之前我们介绍过的一样。但是这样在多维数组中是我们不推荐的。因为在对应关系显示不明显。数组 b 的初始化方式是我们推荐的,这种初始化看上去对应关系比较明了。当然缺点是我们要输入很多个括号。数组 C 是进行了部分初始化,将 c[0][0] 和 c[0][1] 两个位置赋值为 3 和 4,其它位置会自动赋值为 0 。多维数组与一维数组一样,一定要先初始化或者赋值后才能使用。否则也会产生未知的错误。数组 d 展示了采用对应维数的大括号的便捷之处。我们这里只给 d[1][0] 和 d[1][1] 赋值,而没有给前面的两个位置赋值。
将一维的最内层的循环转移到外部循环迭代器,使得 NumPy 的矢量化操作在处理更大规模数据时变得更有效率。简单来说,当指定 flags=['external_loop'] 时,将返回一维数组而并非单个元素。具体来说,当 ndarray 的顺序和遍历的顺序一致时,将所有元素组成一个一维数组返回;当 ndarray 的顺序和遍历的顺序不一致时,返回每次遍历的一维数组。下面通过具体案例来理解这句话:案例对于上述创建的 arr,是行优先顺序的数组。当我们指定遍历顺序为C(行优先,与定义的顺序一致),指定 flags=["external_loop"],则有:for i in np.nditer(arr, flags=['external_loop'], order='C'): print(i)打印结果为:[0 1 2 3 4 5]可以看到,该案例中,把全部元素组成一个一维数组,并返回。案例当我们指定遍历顺序为F(列优先),指定 flags=["external_loop"],则有:for i in np.nditer(arr, flags=['external_loop'], order='F'): print(i)打印结果为:[0 3][1 4][2 5]可以看到,该案例中,返回每次遍历的一维数组。
数组(Array)是一种简单的复合数据类型,它是一组有序数据的集合。数组根据维度可以分为一维数组、二维数组和多维数组。因为数据结构的本质就是存放数据的容器,所以我们可以找到它们在生活中的很多原型。如果把冰箱比作计算机存储的话,我们最常见到的鸡蛋盒就是数组的最佳模型了,他们的很多特点甚至都非常相似。(图片来源于网络,版权归原作者所有)
广播机制的存在,允许运算发生在不同维度的数组之间,其中最典型的场景,是数组的标量的计算。所谓的标量,我们可以简单地将之视为是0维的数组。案例对于加法运算:arr0 + 5out: array([[ 6, 7, 8], [ 9, 10, 11]])对于乘法运算arr0 * 5out: array([[ 5, 10, 15], [20, 25, 30]])对于幂运算:arr0 ** 5out: array([[ 1, 32, 243], [1024, 3125, 7776]], dtype=int32)可以发现,对于上述效果,是数组每个元素分别与标量进行运算的结果。直观地,我们发现,标量在这个二维数组上发生了传播(广播),标量沿着数组的两个维度扩散,直至扩散的结果和待计算的二维数组一致,最后进行计算。
数组的声明形式形如var 数组名 [数组长度]数组类型,其中数组类型可以是数组本身,也就是数组类型的数组,这样就构成了多维数组。和变量的声明相同,数组在声明时会初始化变量类型的零值。代码示例package mainimport ( "fmt")func main() { var a [2]int var b [2][2]int var c = [2]int{1, 2} var d = [...]int{3, 4} fmt.Println("a的零值", a) fmt.Println("b的零值", b) fmt.Println("c的值", c) fmt.Println("d的值", d)}第 8 行:声明一个长度为2的一维数组。自动初始化为零值;第 9 行:声明一个2行2列的二维数组。自动初始化为零值;第 10 行:声明并定义一个长度为2,值为[1,2]的数组;第 11 行:使用 ... 表示根据数组后值的长度自动初始化长度。因为后面的值是 [3,4] 数组自动初始化长度为 2。执行结果:
对于两个一维数组,dot() 函数计算的是这两个数组对应下标元素的乘积和,也称之为內积。对于二维数组,计算的是两个数组的矩阵乘积。案例创建两个一维数组 a 和 b:a = np.array([1, 2, 3, 4])b = np.array([6, 7, 8, 9])计算一维数组的內积:np.dot(a, b)out: 80创建两个二维数组:A = np.array([[1, 2, 3], [4, 5, 6]])B = np.array([[11, 22], [33, 44], [55, 66]])计算二维数组的矩阵乘积:np.dot(A, B)out: array([[242, 308], [539, 704]])可以发现,对二维数组,矩阵的乘积满足如下规律:m×p的矩阵A,p×n的矩阵B,其矩阵乘积结果为大小为m×n的矩阵。
将一个带有键值对的数组转化成对象。var arr = [ ['a', '0'], ['b', '1'], ['c', '2'] ];var obj = Object.fromEntries(arr);console.log(obj); // {a: "0", b: "1", c: "2"}上面的代码中,arr 是一个二维数组,子数组中的每一个项包含键和值,只有这样的数组类型才可以转化为对象。
Map 对象保存的是一个个键值对,Map 中的参数是一个数组或是一个可迭代的对象。 Array.from() 可以把 Map 实例转换为一个二维数组。const map = new Map([[1, 2], [2, 4], [4, 8]]);Array.from(map); // [[1, 2], [2, 4], [4, 8]]
一种简单的场景是,两个数组有一定的相似性,即数组(n×m)和数组(1,m)。案例arr2 = np.array([[10, 20, 30]])arr2out: array([[10, 20, 30]])观察不同大小的数组的广播规则:arr0 + arr2out: array([[11, 22, 33], [14, 25, 36]])arr0 * arr2out: array([[ 10, 40, 90], [ 40, 100, 180]])观察发现,arr2沿着arr0的第二个维度扩展了,扩展到二者相匹配,再进行了对应的计算。可以用一张简图来进行描述相加的过程: 广播 1
numpy.transpose 函数用于对换数组的维度。案例调换 arr0 的数组维度,达到转置的效果:np.transpose(arr0)out: array([[100, 4], [ 1, 5], [ 2, 6], [ 3, 7]])类似地,上述案例可以写为:arr0.transpose()out: array([[100, 4], [ 1, 5], [ 2, 6], [ 3, 7]])或者:arr0.Tout: array([[100, 4], [ 1, 5], [ 2, 6], [ 3, 7]])
对于二维数组,在水平方向进行堆叠,但是数组的维度保持不变。函数效果等价于numpy.concatenate((a1, a2, …), axis=1)。案例利用 hstack 函数合并数组 a 和 b:np.hstack((a,b))out: array([[1, 2, 5, 6], [3, 4, 7, 8]])上述效果与 np.concatenate((a, b), axis=1) 完全一致。
numpy.concatenate 函数用于沿指定轴连接两个或多个相同形状的数组。该函数的原型如下:numpy.concatenate((a1, a2, ...), axis=0)参数说明如下:参数说明a1,a2 …相同类型的数组序列axis连接数组的轴的方向,默认值为0案例创建大小为 2×2 的二维数组 a 和 b:a = np.array([[1,2],[3,4]])b = np.array([[5,6],[7,8]])查看数组:print("数组a:")print(a)print("数组b:")print(b)打印结果为:数组a:[[1 2] [3 4]]数组b:[[5 6] [7 8]]利用 concatenate 函数进行垂直方向的合并:np.concatenate((a, b), axis=0)out: array([[1, 2], [3, 4], [5, 6], [7, 8]])上述语句实现了:沿着 axis=0(对于二维数组,显示为垂直)方向合并,合并的结果在垂直方向的维数扩充到 4,整个结果的数组大小为 4×2。利用 concatenate 函数进行水平方向的合并:np.concatenate((a, b), axis=1)out: array([[1, 2, 5, 6], [3, 4, 7, 8]])上述语句实现了:沿着 axis=1(对于二维数组,显示为水平)方向合并,合并的结果在水平方向的维数扩充到 4,整个结果的数组大小为 1×4。通过观察上述过程,可以发现:concatenate 可以实现数组沿着某一轴向进行合并,合并后数组的维度保持不变。案例numpy.concatenate 函数可以一次性拼接多个数组:np.concatenate((a, b, a, b), axis=1)out: array([[1, 2, 5, 6, 1, 2, 5, 6], [3, 4, 7, 8, 3, 4, 7, 8]])concatenate 函数可以接收不定长的数组序列,并按照指定的轴进行合并。
numpy.append 函数可以在输入数组末尾,追加一个尺寸匹配的数组,与列表中 append 的操作类似。但是输入数组的尺寸必须匹配,否则将生成 ValueError。该函数的原型如下:numpy.append(arr, values, axis)参数说明如下:参数说明arr输入数组values待追加的数组,values在不包括附加轴的情况下,其形状需与arr保持一致axis附加操作的轴。如果没有给出,则两个参数会先折叠为一维案例以二维数组为例,对 arr_2 的 axis=0 方向追加数组:np.append(arr_2, [[1, 2]], axis=0)out: array([[ 12, 23], [ 34, 45], [ 56, 67], [ 78, 89], [100, 110], [120, 130], [ 1, 2]])案例以二维数组为例,对 arr_2 的 axis=1 方向追加数组:np.append(arr_2, [[1], [2], [3], [4], [5], [6]], axis=1)out: array([[ 12, 23, 1], [ 34, 45, 2], [ 56, 67, 3], [ 78, 89, 4], [100, 110, 5], [120, 130, 6]])案例当不指定 axis 时,在追加动作产生前,会先把输入数组展平:np.append(arr_2, [[1], [2], [3], [4], [5], [6]])out: array([ 12, 23, 34, 45, 56, 67, 78, 89, 100, 110, 120, 130, 1, 2, 3, 4, 5, 6])当不指定axis时,append结果为一维数组。
numpy.matmul 函数返回两个数组的矩阵乘积。对于二维数组,其计算结果与dot一致。案例np.matmul(A, B)out: array([[242, 308], [539, 704]])
对于二维数组,在垂直方向进行堆叠,但是数组的维度保持不变。函数效果等价于numpy.concatenate((a1, a2, …), axis=0)。案例利用 hstack 函数合并数组 a 和 b:np.vstack((a,b))out: array([[1, 2], [3, 4], [5, 6], [7, 8]])上述效果与np.concatenate((a, b), axis=0)完全一致。
根据数组的特点我们可以发现,数组的使用场景多是在读取频繁,增减较少最好是不需要增减的场合,在初始化数组的时候能够确定元素的最大个数,比如以下场景:存储某班级学生的语文成绩时可以使用数组,元素长度固定几乎不需要增减,读取高效导入EXCEL模板数据的时候可以使用二维数组来储存临时数据,充分利用了数组读取效率高的特性对于动态增加和减少元素的场景,我们可以使用刚刚提到的 ArrayList,后面的章节我们会对这部分内容做详细介绍。这里分享一个有意思的小插曲,我在制做上面图片敲代码的时候手误写了这样一行代码,小伙伴们可以结合第二行和第三行的执行结果来解释一下第一行代码的实现结果。int [] array = new int[]{};System.out.println(array.length);array[0] = 1;
数组的维数,即数组的秩,用来表征数组轴的数量或维度的数量。对于行向量:arr_1 = np.array([1,2,3])arr_1Out: array([1, 2, 3])这是一个典型的一维数组,其秩记为 1,也就是说行向量只有一个轴(axis)。对于二维数组:arr_2 = np.array([[1,2,3],[4,5,6]])arr_2Out: array([[1, 2, 3], [4, 5, 6]])可以把该二维数组视作是两个一维数组组成,其秩记为 2,也就是说二维数组有两个数据轴。我们可以用下图来表征数组的数据轴:在 Numpy 中数据轴记为 axis,其中 axis=0 记为最外层的轴。在二维数组中对应于列方向(即为上图的垂直方向)。案例通过 ndim 来查看一维数组的秩。arr_1.ndimOut: 1通过ndim来查看二维数组的秩。arr_2.ndimOut: 2
二维数组可以想象成一个存放数据的表格,二维数组的创建和一维数组相似。可以声明数组的同时创建:javaDataType[][] arrayName = new int[行数][列数];也可以先声明再创建:DataType[][] arrayName;arrayName = new DataType[行数][列数];实例:// 声明并创建一个2行3列的数组int[][] intArray = new int[2][3];// 声明一个单精度浮点型的二维数组float floatArray[][];// 创建一个3行3列的二维数组floatArray = new float[3][3];在创建数组的时候,我们也可以先不指定列的数量,再在后面进行逐行创建。实例:// 创建一个3行的二维数组int intArray[][] = new int[3][];// 第一行3列intArray[0] = new int[3];// 第二行2列intArray[1] = new int[2];// 第三行1列intArray[2] = new int[1];
如果第二个参数为负值时,计算数组的长度和第二个参数之和小于 0,则整个数组都会被搜索。var arr = ['a', 'b', 'c'];arr.includes('a', -10); // truearr.includes('b', -10); // truearr.includes('a', -2); // falsearr 的数组长度是 3,第二个参数是 - 10,计算之和为 -7 小于 0,则整个数组都会被搜索。
类数组并不是数组,而是长得像数组的对象。var fakeArray = { 0: '第一项', 1: '第二项', 3: '第三项', length: 3,};console.log(fakeArray[0]); // 输出:"第一项"console.log(fakeArray.length); // 输出:3上述例子中的 fakeArray 就是一个类数组,属性是以类型数组的下标的形式存在,同时也具有 length 属性。这种类数组对象,也被称为 array-like对象 ,部分文献也称为伪数组。类数组对象可以转化为数组,许多方法在设计时也会考虑支持类数组。