目录标题
前言
本文主要介绍java语言中的数组类型
1 数组
1.1 数组的概述
数组(Array):多个相同类型数据按一定顺序排列 的集合,并使用一个名字命名,并通过编号的方式对这些数据进行统一管理。
说明:
- 数组本身是引用数据类型,而数组中的元素可以是任何数据类型,包括基本数据类型和引用数据类型。
- 创建数组对象会在内存中开辟一整块连续的空间,而数组名中引用的是这块连续空间的首地址。
- 数组的长度一旦确定,就不能修改。
- 我们可以直接通过下标(或索引)的方式调用指定位置的元素,速度很快。
- 数组的分类:
① 按照维度:一维数组、二维数组、三维数组、…
② 按照元素的数据类型分:基本数据类型元素的数组、引用数据类型元素的数组(即对象数组)
1.2 一维数组的使用
1.2.1 声明
一维数组的声明方式:
type var[] 或 type[] var(推荐);
注:Java语言中声明数组时不能指定其长度(数组中元素的数)例如: int a[5]; //非法
1.2.2 数组的初始化
动态初始化:数组声明且为数组元素分配空间与赋值的操作分开进行
int[] arr = new int[3];
arr[0] = 3;
arr[1] = 9;
arr[2] = 8;
//---------------------
String names[];
names = new String[3]; names[0] = “钱学森”;
names[1] = “邓稼先”;
names[2] = “袁隆平”;
静态初始化:在定义数组的同时就为数组元素分配空间并赋值。
int arr[] = new int[]{ 3, 9, 8};或int[] arr = {3,9,8};
String names[] = {“李四光”,“茅以升”,“华罗庚”}
说明:
- 可以使用var来定义变量—只要在定义该变量时为其指定初始值即可,这样编译器就可推断出该变量的类型。
// 编译器推断names变量的类型是String[]
var names = new String[] {“Hello”, “World”};
- 使用静态初始化简化语法执行初始化的数组不能使用var定义数组变量。
//‘var’ is not allowed as an element type of an array
//var[] a = {5, 6, 7, 9};
- 不要同时使用静态初始化和动态初始化,也就是说,不要在进行数组初始化时,既指定数组的长度,也为每个数组元素分配初始值。
int arr[] = new int[3]{ 3, 9, 8};//非法
- 执行动态初始化时,程序员只需指定数组的长度,即为每个数组元素指定所需的内存空间,系统将负责为这些数组元素分配初始值。
- 对于基本数据类型而言,默认初始化值各有不同
- 对于引用数据类型而言,默认初始化值为null(注意与0不同!)
1.2.3 数组的引用
数组的引用:
- 定义并用运算符new为之分配空间后,才可以引用数组中的每个元素;
- 数组元素的引用方式:数组名[数组元素下标]
①数组元素下标可以是整型常量或整型表达式。如a[3] , b[i] , c[6*i];
②数组元素下标从0开始;长度为n的数组合法下标取值范围: 0 —>n-1;如int a[]=new int[3]; 可引用的数组元素为a[0]、a[1]、a[2]
③如果访问数组元素时指定的索引值小于0,或者大于等于数组的长度,编译程序不会出现任何错误,但运行时出现异常:java.lang.ArrayIndexOutOfBoundsException:N(数组索引越界异常) - 每个数组都有一个属性length指明它的长度,例如:a.length 指明数组a的长
度(元素个数)
①数组一旦初始化,其长度是不可变的
数组的遍历:
- 普通for循环遍历:
@Test
public void arrayTest() {
//数组的定义和初始化同时完成,使用动态初始化语法
int[] prices = new int[5];
// 使用普通for循环输出prices数组的每个数组元素的值
for (var i = 0; i < prices.length; i++)
{
System.out.println(prices[i]);
}
}
- foreach循环(java5以及以后)
foreach循环:循环遍历数组和集合,更加简洁。无须获得数组和集合长度,无须根据索引来访问数组元素和集合元素,foreach循环自动遍历数组和集合的每个元素。
foreach 循环的语法格式如下∶
for(type variableName : array l collection){
// variableName 自动迭代访问每个元素…
}
@Test
public void arrayTest1() {
//数组的定义和初始化同时完成,使用动态初始化语法
int[] prices = new int[5];
// 使用 foreach循环输出prices数组的每个数组元素的值
for (int i : prices) {
System.out.println(prices[i]);
}
}
1.2.4 数组的内存解析
内存的简化结构:
1.3 多维数组的使用
本节主要介绍二维数组
说明:
- Java 语言里提供了支持多维数组的语法。
- 如果说可以把一维数组当成几何中的线性图形, 那么二维数组就相当于是一个表格。
- 对于二维数组的理解,我们可以看成是一维数组 array1又作为另一个一维数组array2的元素而存 在。其实,从数组底层的运行机制来看,其实没 有多维数组。
1.4 数组经典练习题以及常用算法
1.4.1 练习题
1.4.1.1 使用二维数组打印杨辉三角
@Test
public void arrayTest2() {
/*
* 提示:
* 1.第一行有1个元素,第n行有n个元素
* 2.每一行的第一个元素和最后一个元素都是1
* 3.从第3行开始,对于非第一个元素和最后一个元素。即:
* arr[i][j]=arr[i-1][j-1]+arr[i-1][j];
*/
//1.声明并初始化二维数组
Scanner sc =new Scanner(System.in);
System.out.println("请输入要打印的行数:");
int nextInt = sc.nextInt();
int[][] arr=new int[nextInt][];
//2.给数组元素赋值
for (int i = 0; i < arr.length; i++) {
//初始化列数
arr[i]=new int[i+1];
//给首末元素赋值
arr[i][0]=arr[i][i]=1;
//给每行的非首末元素赋值(第3行开始)
if(i>1) {
for (int j = 1; j < arr[i].length-1; j++) {
arr[i][j]=arr[i-1][j-1]+arr[i-1][j];
}
}
}
//3.遍历二维数组
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j]+" ");
}
System.out.println();
}
}
1.4.1.2 随机赋值
@Test
public void arrayTest2() {
/*
* 创建一个长度为6的int型数组,要求取值为1-30,同时元素值各不相同
*/
//初始化数组
int[] arr = new int[6];
for (int i = 0; i < arr.length; i++) {
//数组随机赋值
// [0,1) [0,30) [1,31)
arr[i] = (int) (Math.random() * 30) + 1;
//与前面元素作比较,相同重新赋值
for (int j = 0; j < i; j++) {
if (arr[i] == arr[j]) {
i--;
break;
}
}
}
//循环遍历数组
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
1.4.1.3 回形数格式方阵的实现
@Test
public void arrayTest2() {
/*
* 从键盘输入一个整数(1~20)
则以该数字为矩阵的大小,把1,2,3…n*n 的数字按照顺时针螺旋的形式填入其中。例如: 输入数字2,则程序输出:
1 2
4 3
输入数字3,则程序输出:
1 2 3
8 9 4
7 6 5
输入数字4, 则程序输出:
1 2 3 4
12 13 14 5
11 16 15 6
10 9 8 7
*/
Scanner sc=new Scanner(System.in);
System.out.println("请输入一个数字:");
int n = sc.nextInt();
int[][] arr=new int[n][n];
int num=0;
//列
int c1=0;int c2=n-1;
//行
int r1=0;int r2=n-1;
while(c1<=c2) {
//第r1行
for (int i = c1; i <=c2; i++) {
arr[r1][i]=++num;
}
//第c2列
for (int i = r1+1; i <=r2; i++) {
arr[i][c2]=++num;
}
//第r2行
for (int i = c2 - 1; i >= c1 ; i--) {
arr[r2][i] = ++num;
}
//第c1列
for (int i = r2 - 1; i > r1; i--) {
arr[i][c1] = ++num;
}
c1++;
c2--;
r1++;
r2--;
}
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[0].length; j++) {
System.out.print(arr[i][j] + "\t");
}
System.out.println();
}
}
1.4.2 常用算法
数组中涉及到的常见算法:排序算法
排序:假设含有n个记录的序列为{R1,R2,…,Rn},其相应的关键字序列为{K1,K2,…,Kn}。将这些记录重新排序为{Ri1,Ri2,…,Rin},使得相应的关键字值满足条Ki1<=Ki2<=…<=Kin,这样的一种操作称为排序。
- 通常来说,排序的目的是快速查找
衡量排序算法的优劣:
- 时间复杂度:分析关键字的比较次数和记录的移动次数
- 空间复杂度:分析排序算法中需要多少辅助内存
- 稳定性:若两个记录A和B的关键字值相等,但排序后A、B的先后次序保持不变,则称这种排序算法是稳定的。
排序算法分类:内部排序和外部排序。
- 内部排序:整个排序过程不需要借助于外部存储器(如磁盘等),所有排序操作都在内存中完成。
- 外部排序:参与排序的数据非常多,数据量非常大,计算机无法把整个排序过程放在内存中完成,必须借助于外部存储器(如磁盘)。外部排序最常见的是多路归并排序。可以认为外部排序是由多次内部排序组成。
算法的5大特征:
说明:
满足确定性的算法也称为:确定性算法。现在人们也关注更广泛的概念,例如:考虑各种非确定性的算法,如并行算法、概率算法等。另外,人们也关注并不要求终止的计算描述,这种描述有时被称为过程(procedure)。
算法的性能对比:
各种内部排序方法性能比较:
- 从平均时间而言:快速排序最佳。但在最坏情况下时间性能不如堆排序和归并排序。
- 从算法简单性看:由于直接选择排序、直接插入排序和冒泡排序的算法比较简单,将其认为是简单算法。对于Shell排序、堆排序、快速排序和归并排序算法,其算法比较复杂,认为是复杂排序。
- 从稳定性看:直接插入排序、冒泡排序和归并排序时稳定的;而直接选择排序、快速排序、 Shell排序和堆排序是不稳定排序
- 从待排序的记录数n的大小看,n较小时,宜采用简单排序;而n较大时宜采用改进排序
排序算法的选择 :
- 若n较小(如n≤50),可采用直接插入或直接选择排序。当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插入,应选直接选择排序为宜。
- 若文件初始状态基本有序(指正序),则应选用直接插入、冒泡或随机的快速排序为宜;
- 若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
十大内部排序算法:
选择排序:
- 直接选择排序(手写)、堆排序
交换排序 :
- 冒泡排序、快速排序 (手写)
插入排序 :
- 直接插入排序、折半插入排序、Shell排序
归并排序
桶式排序
1.4.2.1 直接选择排序
public static void selectSort(int[] data) {
System.out.println("开始排序");
int arrayLength = data.length;
for (int i = 0; i < arrayLength - 1; i++) {
for (int j = i + 1; j < arrayLength; j++) {
if (data[i] - data[j] > 0) {
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}
}
System.out.println(java.util.Arrays.toString(data));
}
}
1.4.2.2 冒泡排序(手写)
介绍:
冒泡排序的原理非常简单,它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。
排序思想:
- 比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较为止。
public static void selectSort(int[] arr) {
System.out.println("开始排序");
for (int i = 0; i < arr.length-1; i++) {
for (int j = 0; j < arr.length-1-i; j++) {
if (arr[j]>arr[j+1]) {
int temp=arr[j+1];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
for(int i = 0;i < arr.length;i++){
System.out.print(arr[i] + "\t");
}
}
1.4.2.3 快速排序(手写)
介绍:
快速排序通常明显比同为O(nlogn)的其他算法更快,因此常被采用,而且快排采用了分治法的思想,所以在很多笔试面试中能经常看到快排的影子。可见掌握快排的重要性。快速排序(Quick Sort)由图灵奖获得者Tony Hoare发明,被列为20世纪十大算法之一,是迄今为止所有内排序算法中速度最快的一种。冒泡排序的升级版,交换排序的一种。快速排序的时间复杂度为O(nlog(n))。
排序思想:
- 从数列中挑出一个元素,称为"基准"(pivot),
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
- 递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
一次划分的具体过程:
1.low指向待划分区域首元素,high指向待划分区域尾元素;
2.R【0】=R【low】(为了减少数据的移动,将作为标准的元素暂有到R【0】中,最后再放入最终位置);
3.high从后往前移动直到R【high】.key<R【0】.key;
4. R[low]]=R[high], low++;
5.low从前往后移动直到R【low】.key>=R【0】.key;
6. R[high]=R[low], high–;
7. goto 3;
8.直到low==high时,R【low】=R【0】(即将作为标准的元素放到
其最终位置)。
概括地说,一次划分就是从表的两端交替地向中间进行扫描,将小的放到左边,大的放到右边,作为标准的元素放到中间。
public static void quikSort(int[] data, int start, int end) {
if (start < end) {
// R【0】=R【low】
int base = data[start];
int low = start;
int high = end;
while (low < high) {
// high从后往前移动直到R【high】.key<R【0】.key;
while (low < high && data[high] >= base) {
high--;
}
if (low < high && data[high] < base) {
data[low] = data[high];
low++;
}
// low从前往后移动直到R【low】.key>=R【0】.key;
while (low < high && data[low] < base) {
low++;
}
if (low < high && data[low] >= base) {
data[high] = data[low];
high--;
}
}
// 直到low==high时,R【low】=R【0】(即将作为标准的元素放到其最终位置)。
if (low == high) {
data[low] = base;
}
quikSort(data, start, high - 1);
quikSort(data, high + 1, end);
}
}
1.5 和数组相关的异常
①ArrayIndexOutOfBoundsException数组索引越界异常。访问到了不存在的位置(编译不会报错)。
②NegativeArraySizeException分配了负长度异常(编译不会报错)。
③NullPointerException空指针异常,当null调用了任何的方法或者是任何的属性,就会触发该异常(编译不会报错)。