Java 中的一维数组以及二维数组(8千字零基础超全详解)
文章目录
前言
- 数组可以存放多个同一类型的数据。数组也是一种数据类型,是引用类型。 即:数(数据)组(一组)就是一组数据。
一、一维数组
1. 数组的初始化
1.1 动态初始化一
- 基本语法:
- 说明:int[] double = new double[5] 与 int double[] = new double[5] 这两者是等价的,习惯使用前者。
- 数组的引用=数组的使用=数组的访问=获取数组元素;这几种说法不同,但本质相同。
1.2 动态初始化二
- 基本语法:
- 动态初始化的第二种方式是先声明一个数组的存在,但是先不给它分配内存空间,等到需要的时候再给它分配内存空间。
1.3 静态初始化
- 基本语法:
- 静态初始化数组,是在声明数组的同时便给数组的每个元素进行赋值;而动态初始化数组,数组中的每个元素是默认的初始值,需要我们另外遍历数组进行赋值。
2. 数组使用的注意事项和细节
- 数组是多个相同类型数据的组合,实现对这些数据的统一管理 ;
- 数组中的元素可以是任何相同的数据类型,包括基本类型和引用类型,但是不能混用;也可以存在向上转型的非相同的数据类型,如 double[] 中可以有元素int,但是会自动向上转型为double型。
- 数组创建后,如果没有赋值,有默认值:int 0,short 0,byte 0,long 0,float 0.0,double 0.0,char \u0000,boolean false,String null ;
- 使用数组的步骤: 1. 声明数组并开辟空间; 2. 给数组各个元素赋值; 3. 使用数组;
- 数组的下标是从 0 开始的;
- 数组下标必须在指定范围内使用,否则报错:下标越界异常,比如 int [] arr = new int[5],则有效下标为 0-4;输出arr[5]会报错;
- 数组属引用类型,数组型数据是对象(object)。
3. 经典例题
请求出一个数组 int[]的最大值 {4, -1, 9, 10, 23},并得到其对应的下标
思路分析:
- 定义一个 int 数组 int[] arr = {4,-1,9, 10,23};
- 假定 max = arr[0] 是最大值 , maxIndex=0;
- 从下标 1 开始遍历 arr, 如果 max < 当前元素,说明 max 不是真正的最大值, 我们就 max=当前元素; maxIndex=当前元素下标;
- 当我们遍历这个数组 arr 后 , max 就是真正的最大值,maxIndex 最大值对应的下标。
- 代码实现:
public class ArrayExercise02 {
//编写一个 main 方法
public static void main(String[] args) {
int[] arr = {4,-1,9,10,23};
int max = arr[0];
//假定第一个元素就是最大值
int maxIndex = 0;
for(int i = 1; i < arr.length; i++) {
//从下标 1 开始遍历 arr
if(max < arr[i]) {
//如果 max < 当前元素
max = arr[i]; //把 max 设置成 当前元素
maxIndex = i;
}
}
//当我们遍历这个数组 arr 后 , max 就是真正的最大值,maxIndex 最大值下标
System.out.println("max=" + max + " maxIndex=" + maxIndex);
}
}
4. 数组赋值机制(重难点!)
- 基本类型的赋值方式是值传递/值拷贝,这两种说法本质上是相同的;
- 引用类型的赋值方式是引用传递/地址拷贝,这两种说法本质上是相同的;
- 举例说明(内存图):
- 代码解释:
// 基本类型的赋值
int n1 = 10;// 给基本类型变量赋值
int n2 = n1;// 将n1的值赋值给n2,实质上等同于int n2 = 10
n2 = 80;// 将80赋值给n2,不会影响n1 的值
// 在基本类型的赋值中,赋予给变量的是一个确定的值,每个变量对应着不同的栈内存空间,彼此之间不会相互影响。
// 引用类型的赋值
int[] arr1 = {1, 2, 3};// 在栈内存中声明了一个数组类型变量arr1,在堆内存中为该变量开辟了一个长度为3 的内存空间
int[] arr2 = arr1;// 将arr1的内存地址赋值给arr2,此时arr1和arr2共用一个堆内存空间
arr2[0] = 10;// 将100赋值给arr2中的第一个元素,相当于将10赋值给地址中的第一个元素,同时相当于改变了arr1中的第一个元素的值
// 在引用类型的赋值中,赋予给变量的是内存地址,此时多个变量对应着同一个堆内存空间,彼此之间相互影响。
5. 数组拷贝
- 那怎么创建一个和原数组相同,且内存空间相互独立,不会相互影响的新数组?答:需要新声明一个数组,然后采用for 循环遍历原数组,将原数组中的元素赋值给新数组。
- 代码如下:
public class ArrayCopy {
public static void main(String[] args) {
//将 int[] arr1 = {10,20,30}; 拷贝到 arr2 数组,
//要求数据空间是独立的. int[] arr1 = {10,20,30};
//创建一个新的数组 arr2,开辟新的数据空间 //大小 arr1.length;
int[] arr2 = new int[arr1.length];
//遍历 arr1 ,把每个元素拷贝到 arr2 对应的元素位置
for(int i = 0; i < arr1.length; i++) {
arr2[i] = arr1[i];
}
//修改 arr2中的元素, 不会对 arr1 有影响.
arr2[0] = 100;
//输出 arr1
System.out.println("====arr1 的元素====");
for(int i = 0; i < arr1.length; i++) {
System.out.println(arr1[i]);// 10,20, 30
}
System.out.println("====arr2 的元素====");
for(int i = 0; i < arr2.length; i++) {
System.out.println(arr2[i]);//
}
}
}
6. 数组翻转
- 将数组中的元素对称翻转;
- 代码如下:
// 方法一:观察规律
public class ArrayReverse {
//编写一个 main 方法
public static void main(String[] args) {
//定义数组
int[] arr = {11, 22, 33, 44, 55, 66};
int temp = 0;
int len = arr.length;
//计算数组的长度
for( int i = 0; i < len / 2; i++) {
temp = arr[len - 1 - i];//保存
arr[len - 1 - i] = arr[i]; arr[i] = temp;
}
System.out.println("===翻转后数组===");
for(int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");//66,55,44,33,22,11
}
}
}
// 方法二:采用逆序赋值
public class ArrayReverse02 {
//编写一个 main 方法
public static void main(String[] args) {
//定义数组
int[] arr = {11, 22, 33, 44, 55, 66};
//使用逆序赋值方式
//老韩思路
//1. 先创建一个新的数组 arr2 ,大小 arr.length
//2. 逆序遍历 arr ,将 每个元素拷贝到 arr2 的元素中(顺序拷贝)
//3. 建议增加一个循环变量 j -> 0 -> 5
int[] arr2 = new int[arr.length];
//逆序遍历 arr
for(int i = arr.length - 1, j = 0; i >= 0; i--, j++) {
arr2[j] = arr[i];
}
//4. 当 for 循环结束,arr2 就是一个逆序的数组 {66, 55, 44,33, 22, 11}
//5. 让 arr 指向 arr2 数据空间, 此时 arr 原来的数据空间就没有变量引用会被当做垃圾,销毁 arr = arr2;
System.out.println("====arr 的元素情况=====");
//6. 输出 arr 看看
for(int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
}
}
- 思考:在方法二中,使用了引用类型变量的赋值方式引用传递,让原数组和新数组共用了一个堆内存空间,此时原数组的地址发生了改变,原地址因为没有数组的使用而被虚拟机回收清理。
7. 数组扩容
- 要求:实现动态的给数组添加元素,实现对数组扩容。
- 原始数组使用静态分配 int[] arr = {1,2,3} ;
- 添加的元素 4,直接放在数组的最后 arr = {1,2,3,4} ;
- 用户可以通过如下方法来决定是否继续添加,添加成功,是否继续?y/n
- 思路分析:
1. 定义初始数组 int[] arr = {1,2,3} //下标 0-2;
2. 定义一个新的数组 int[] arrNew = new int[arr.length+1];
3. 遍历 arr 数组,依次将 arr 的元素拷贝到 arrNew 数组 ;
4. 将 4 赋给 arrNew[arrNew.length - 1] = 4;把 4 赋给 arrNew 最后一个元素 ;
5. 让 arr 指向 arrNew ; arr = arrNew;原来分配给 arr的堆内存空间会被销毁 ;
6. 创建一个 Scanner 可以接受用户输入 ;
7. 因为用户什么时候退出,不确定,使用 do-while + break 来控制 。
- 代码如下:
public class ArrayAdd02 {
public static void main(String[] args) {
//要求:实现动态的给数组添加元素效果,实现对数组扩容。
Scanner myScanner = new Scanner(System.in);
//初始化数组
int[] arr = {1,2,3};
do {
int[] arrNew = new int[arr.length + 1];
//遍历 arr 数组,依次将 arr 的元素拷贝到 arrNew 数组
for(int i = 0; i < arr.length; i++) {
arrNew[i] = arr[i];
}
System.out.println("请输入你要添加的元素");
int addNum = myScanner.nextInt();
//把 addNum 赋给 arrNew 最后一个元素
arrNew[arrNew.length - 1] = addNum;
//让 arr 指向 arrNew,
arr = arrNew;
//输出 arr 看看效果
System.out.println("====arr 扩容后元素情况====");
for(int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
//问用户是否继续
System.out.println("是否继续添加 y/n");
char key = myScanner.next().charAt(0);
if( key == 'n') {
//如果输入 n ,就结束
break;
}
} while(true);
System.out.println("你退出了添加...");
}
}
8. 数组缩减
- 数组缩减的思路是和数组扩容几乎完全一样的。
- 代码如下:
public class ArrayAdd02 {
public static void main(String[] args) {
Scanner myScanner = new Scanner(System.in);
//初始化数组
int[] array1 = {1, 2, 3, 4, 5};
do {
int[] array2 = new int[array1.length-1];
for (int i = 0; i < array2.length; i++) {
if (i < array1.length) {
array2[i] = array1[i];
}
}
array1 = array2;
for (int i = 0; i < array1.length; i++) {
System.out.print(array1[i]);
}
if (array1.length == 1) {
System.out.println("数组中只剩下最后一个元素,不能再缩减!");
break;
}
System.out.println("你是否还要缩减y/n");
char c = sc.next().charAt(0);
if (c == 'n') {
break;
}
} while (true);
}
}
二、排序
- 排序的分类:
- 内部排序:指将需要处理的所有数据都加载到内部存储器中进行排序。包括(交换式排序法、选择 式排序法和插入式排序法);
- 外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。包括(合并排序法和直接合并排序法)。
- 此处只介绍一种简单的排序:冒泡排序;其他的排序算法,博主在以后会专门写一遍博客来分享。
- 冒泡排序(Bubble Sorting)的基本思想 :通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素 的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
举例解释:将一个无序的数组:{24, 69, 80, 57, 13} 使用冒泡排序法将其排成一个从小到大的有序数组。
- 思路分析:图示
- 代码实现:
public class BubbleSort {
public static void main(String[] args) {
// 冒泡排序
// 补充:优化策略:如果在某一次比较中,没有元素交换,则表示已经排序好了,可以break;(不用理)
int[] arr = {24, 69, 80, 57, 13};
// 有n个元素,只需要排序n-1轮,每一轮会将一个元素放到正确的位置
// 则剩下的最后一个元素就不需要排序了
for (int i = 0; i < arr.length - 1; i++) {
int tmp = 0;// 用于辅助交换的变量
// 有n个元素,每一轮只需要比较n-1次,n是在不断变化的
for (int k = 0; k < arr.length - i - 1; k++) {
// 如果前面的元素>后面的元素,就交换
if (arr[k] > arr[k + 1]) {
tmp = arr[k];
arr[k] = arr[k + 1];
arr[k + 1] = tmp;
}
}
}
// 遍历输出排序后的数组
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
}
三、查找
- 在 java 中,我们常用的查找有两种:
- 顺序查找
- 二分查找【二分法,此处不涉及】
举例:有一个数组 {1, 3, 5, 7, 9},从键盘中任意输入的一个数,要求: 如果找到了,就提示找到,并给出下标值;否则提示未找到。
- 代码实现:
public class SeqSearch {
public static void main(String[] args) {
// 顺序查找
Scanner sc = new Scanner(System.in);
int[] arr = {1, 3, 5, 7, 9};
System.out.println("请输入一个数:");
int input = sc.nextInt();
int index = -1;// 一个编程技巧,用来判断是否进入一个代码块
for (int i = 0; i < arr.length; i++) {
if (input == arr[i]) {
System.out.println("你找到了" + arr[i]);
index = i;// 一个标识符,用来判断是否成功进入该代码块,若进入,则改变该标识符的值,否则该标识符的值仍为初始值
break;// 找到该元素,直接退出循环
}
}
// index 未改变,说明没有找到需要的元素
if (index == -1) {
System.out.println("该数组中没有你要找的数!");
}
}
}
四、二维数组
1. 二维数据的基本概念
什么是二维数组?
-
从定义形式上看:
int [][]
; -
可以这样理解,一个一维数组中的每个元素都是一维数组,这样就构成了二维数组。
举例:
int[][] arr = { {0, 0, 0, 0, 0, 0}, {1, 1, 1, 1, 1, 1},
{2, 2, 2, 2, 2, 2}, {3, 3, 3, 3, 3, 3}
};
关于二维数组的关键概念:
-
二维数组的元素个数 = 二维数组中一维数组的个数 =
arr.length
; -
二维数组的每个元素是一维数组,所以如果需要得到每个一维数组中的元素,还需要再遍历一维数组;
-
arr[i][j]
表示:二维数组的第i+1
个一维数组的第j+1
个元素;- eg:
arr[0][0]
表示:二维数组arr
中的第1个一维数组中的第1个元素。
- eg:
例子:输出二维数组中的一维数组中的每一个元素
public class TwoDimensionalArray01 {
public static void main(String[] args) {
int[][] arr = { {0, 0, 0, 0, 0, 0}, {1, 1, 1, 1, 1, 1},
{2, 2, 2, 2, 2, 2}, {3, 3, 3, 3, 3, 3}
};
// 遍历二维数组中的每个元素(一维数组)
for(int i = 0; i < arr.length; i++) {
// 1. arr[i] 表示:二维数组的第 i+1 个一维数组。比如 arr[0]:二维数组的第一个一维数组
// 2. arr[i].length 得到对应的每个一维数组的长度
for(int j = 0; j < arr[i].length; j++) {
// 输出二维数组中的一维数组中的每一个元素
System.out.print(arr[i][j] + " ");
}
System.out.println();//换行
}
}
}
控制台输出:
由控制台的输出,我们可以简单理解二维数组的形式:一个二维数组中有几个一维数组,这个二维数组就有几行;而每个一维数组中的元素的个数,就是该行对应的列数。
2. 二维数组的初始化
2.1 静态初始化
基本语法:
定义数组并同时为数组中的所有元素赋值:数据类型[][] 数组名 = {{值1, 值2, ..}, {值1, 值2, ..}, {值1, 值2, ..}};
- 举例:
int[][] arr = {{1, 1, 1}, {8, 8, 9}, {10}};
注意:二维数组的每个元素必须是一维数组,不允许是其他数据类型;每个一维数组中的元素个数可以相等/不相等。上例中,第1个一维数组有 3 个元素,第2个一维数组有 3 个元素,而第三个一维数组只有 1 个元素。
代码:
int[][] arr = {{1, 1, 1}, {8, 8, 9}, {10}};
// 遍历二维数组中的每个元素(一维数组)
for(int i = 0; i < arr.length; i++) {
// 1. arr[i] 表示:二维数组的第 i+1 个一维数组。比如 arr[0]:二维数组的第一个一维数组
// 2. arr[i].length 得到对应的每个一维数组的长度
for(int j = 0; j < arr[i].length; j++) {
// 输出二维数组中的一维数组中的每一个元素
System.out.print(arr[i][j] + " ");
}
System.out.println();// 换行
}
控制台输出:
显然,上例中的二维数组中包含了3个一维数组,因此该二维数组有3行,而每个一维数组中的元素个数不同,所以每一行的列数都不同。
2.2 动态初始化
2.2.1 第一种方式(列数确定)
基本语法:
-
先定义数组:
数据类型[][] 数组名 = new 数据类型[数组大小][数组大小];
- 举例:
int arr[][] = new int[2][3];
- 举例:
-
再给一维数组中的每个元素赋值,若不赋值,则为定义的数据类型的默认值。
这种初始化方式在声明二维数组的同时开辟了其所需要的堆内存空间,这种方式创建出的每一个一维数组的空间都是一样的(即二维数组的列数是一样的)。
代码:
// 定义一个二维数组,同时为数组开辟内存空间,此时二维数组的行数为3,列数为2。也就是我们所说的列数相同
int[][] arr = new int[3][2];
// 然后再遍历所有的一维数组,给其中的每个元素赋值。省略...
2.2.2 第二种方式(列数确定)
基本语法:
- 先声明:
数据类型[][] 数组名;
。 - 再定义(开辟空间):
数组名 = new 数据类型[数组大小][数组大小];
。此时每个一维数组的内存空间都已经开辟。 - 最后给一维数组中的每个元素赋值,若不赋值,则为定义的数据类型的默认值。
第二种方式其实是和第一种方式大同小异的。第一种方式是在声明的同时定义;而第二中是先声明数组,后面需要用到的时候再定义。
代码:
// 先声明一个二维数组
int[][] arr;
// 再为数组开辟内存空间,此时二维数组的行数为3,列数为2。也就是我们所说的列数相同
arr = new int[3][2];
// 最后再遍历所有的一维数组,给其中的每个元素赋值。省略...
2.2.3 第三种方式(列数不确定)
基本语法:
- 先声明:
数据类型[][] 数组名;
。 - 再定义(开辟空间):
数组名 = new 数据类型[数组大小][];
。 注意:此时所有的一维数组都未开辟堆内存空间。 - 遍历二维数组,给一维数组开辟堆内存空间,此时给每个一维数组开辟的内存空间可以不相等(也就是说二维数组的列数是不一样的)。
- 最后给一维数组中的每个元素赋值。若不赋值,则为定义的数据类型的默认值。
代码:
// 定义二维数组。此时所有的一维数组还没有分配内存空间;arr[i]的地址为null;
int[][] arr = new int[3][];
// 遍历二维数组,给每个一维数据开辟内存空间。这种方式会使得每个一维数组的大小可以不一样
for (int i = 0; i < arr.length; i++) {
// 给每个一维数组开辟空间,若没有这一步,一维数组的内存空间就是 null;
arr[i] = new int[i + 1];
}
// 最后再遍历所有的一维数组,给其中的每个元素赋值。省略...
3. 二维数组的内存存在形式
以动态初始化的第一种方式为例,int arr[][] = new int[2][3];
其内存示意图如下:
示意图说明:
- 首先在栈内存中声明了一个
int
类型的二维数组的arr[2][3]
,于是JVM
在堆内存中为arr
开辟了一个大小为 2 的地址空间,这两个空间里面分别存储着一维数组arr[0]
、arr[1]
的地址; - 同时在堆内存中另外开辟了两个新的大小为 3 的内存空间,这两个空间的地址就是
arr[0]
、arr[1]
的内存地址,空间里面存储着一维数组中的元素。
注意:如果是以第三种方式(列数不确定)创建一个二维数组,例如:
int[][] arr = new int[3][];
。则此时所有的一维数组都没有开辟内存空间,它们的地址都为 null
;
4. 二维数组使用细节和注意事项
一维数组的声明方式有: int[] x
或者 int x[]
。
二维数组的声明方式有: int[][] arr
或者 int[] arr[]
或者 int arr[][]
。
二维数组实际上是由多个一维数组构成的,它的各个一维数组的内存大小可以相同,也可以不相同。
- 比如:
map[][]
是 一个二维数组,int[][] map = {{1, 2}, {3, 4, 5}}
;其中,map[0]
是一个含有2个元素的一维数组 ,map[1]
是一个含有3个元素的一维数组,因此,map[][]
也称为 “列数不等的二维数组”。
5. 二维数组的经典案例:杨辉三角
案例一:使用二维数组打印一个 10 行 杨辉三角,如下图所示:
规律:
- 第一行有 1 个元素, 第 n 行有 n 个元素 ;
- 每一行的第一个元素和最后一个元素都是 1 ;
- 从第三行开始, 对于非第一个元素和最后一个元素的元素的值
arr[i][j]
,arr[i][j] = arr[i-1][j] + arr[i-1][j-1]
。
代码实现:
public class TwoDimentionalArray01 {
public static void main(String[] args) {
int[][] a = new int[10][];
for (int i = 0; i < a.length; i++) {
// 给每个一维数组开辟堆内存空间,第n行有n个元素
a[i] = new int[i + 1];
// 遍历每一个一维数组,赋值
for (int j = 0; j < a[i].length; j++) {
// 每一行的第一个元素和最后一个元素都是1
if (j == 0 || j == a[i].length - 1) {
a[i][j] = 1;
} else {
// 每一行非第一个元素和最后一个元素的值 = 上一行的同一列 + 上一行的上一列
a[i][j] = a[i - 1][j] + a[i - 1][j - 1];
}
}
}
// 输出杨辉三角
for (int i = 0; i < a.length; i++) {
for (int k = 0; k < a[i].length; k++) {
System.out.print(a[i][k] + "\t");
}
System.out.println();
}
}
}
总结
本文是小白博主在学习B站韩顺平老师的Java网课时整理的学习笔记,在这里感谢韩顺平老师的网课,如有有兴趣的小伙伴也可以去看看。
最后,如果本文有什么错漏的地方,欢迎大家批评指正!一起加油!!
更多推荐
所有评论(0)