3.1 数组的概述
- 数组(Array),是多个相同类型数据按一定顺序排列的集合,并使用一个名字命名,并通过编号的方式对这些数据进行统一管理。
- 数组的常见概念:数组名、下标(或索引) 、元素、数组的长度。
- 数组本身是引用数据类型,而数组中的元素可以是任何数据类型,包括基本数据类型和引用数据类型。
- 创建数组对象会在内存中开辟一整块连续的空间,而数组名中引用的是这块连续空间的首地址。
- 数组的长度一旦确定,就不能修改。
- 我们可以直接通过下标(或索引)的方式调用指定位置的元素,速度很快。
- 数组的分类:(1)按照维度:一维数组、二维数组、三维数组、…(2)按照元素的数据类型分:基本数据类型元素的数组、引用数据类型元素的数组(即对象数组)。
3.2 一维数组的使用
3.2.1 一维数组的声明和初始化
什么是声明和初始化?
// 变量的声明和初始化
int num; // 声明
num = 10; // 初始化
int id = 1001; // 声明+初始化
3.2.1.1 一维数组的声明
一维数组的声明方式:
type var[]
// 或
type[] var;
例如:
int a[];
int[] a1;
double b[];
String[] c; //引用类型变量数组
-Java语言中声明数组时不能指定其长度(数组中元素的个数), 例如:
// int a[5]; // 非法语句
3.2.1.2 一维数组的初始化
动态初始化:数组声明且为数组元素分配空间与赋值的操作分开进行。
// 方式一:声明类型、声明个数、赋值分开
type[] name; // 或 type name[]; // 声明类型
name = new type[length]; // 声明元素个数
nam[0] = ele0; // 赋值
name[1] = ele1;
...
// 方式二:
type[] name = new type[length]; // 声明类型和个数
name[0] = ele0; // 赋值
name[1] = ele1;
...
静态初始化:在声明数组的同时就为数组元素分配空间并赋值。
type[] name = new type[]{ele0,ele1,ele2}; // 推荐
type name[] = new type[]{ele0,ele1,ele2};
type[] name = {ele0,ele1,ele2};
举例:
举例:
// 动态初始化方式一
int[] ids1;
ids1 = new int[3];
ids1[0] = 10; // 可以不用立即全部元素赋值完
// 动态初始化方式二
int ids2[] = new int[2];
ids2[1] = 21; // 不一定要按顺序赋值
// 静态初始化
int[] ids3 = new int[]{30,31,32};
int[] ids4 = {40,41,42,43,44};
总结:数组初始化完成后,其长度就确定了;String[] 为数组类型,String是数组元素的类型。
3.2.2 数组元素的引用
- 定义并用运算符new为之分配空间后,才可以引用数组中的每个元素;
- 数组元素的引用方式:数组名[数组元素下标]
- 数组元素下标可以是整型常量或整型表达式。如a[3] , b[i] , c[6*i];
- 数组元素下标从0开始;长度为n的数组合法下标取值范围: 0 —>n-1;如int a[]=new int[3]; 可引用的数组元素为a[0]、a[1]、a[2]
- 每个数组都有一个属性length指明它的长度,例如:a.length 指明数组a的长度(元素个数)
- 数组一旦初始化,其长度是不可变的
// 输出数组长度
System.out.println(names.length);
- 数组是引用类型,它的元素相当于类的成员变量,因此数组一经分配空间,其中的每个元素也被按照成员变量同样的方式被隐式初始化。例如:
public class Test {
public static void main(String argv[]){
int a[]= new int[5];
System.out.println(a[3]); //a[3]的默认值为0 }
}
注意:
- 对于基本数据类型而言,默认初始化值各有不同 ;
- 对于引用数据类型而言,默认初始化值为null(注意与0不同!)。
3.2.3 遍历数组
int[] ids = {0,1,2,3,4,5};
for(int i=0;i<ids.length;i++){
System.out.println(ids[i]);
}
3.2.4 数组元素的默认初始化值
引用类型的变量只能存储两类值:null 或 地址值
3.2.5 内存解析
方法中的变量都是局部变量。
变量名在栈中声明,new后面的在堆中开辟存储空间,二者通过内存地址来对应。
上面arr1重新new了一次,所以在堆空间重新开辟了一处内存空间对应。
练习1:升景坊单间短期出租4个月,550元/月(水电煤公摊,网费35元/月),空调、卫生间、厨房齐全。屋内均是IT行业人士,喜欢安静。所以要求来租者最好是同行或者刚毕业的年轻人,爱干净、安静。
public class ArrayTest {
public static void main(String[] args) {
int[] arr = new int[]{8,2,1,0,3};
int[] index = new int[]{2,0,3,2,4,0,1,3,2,3,3};
String tel = "";
for(int i = 0;i < index.length;i++){
tel += arr[index[i]];
}
System.out.println("联系方式:" + tel);
}
}
练习2:从键盘读入学生成绩,找出最高分, 并输出学生成绩等级。 成绩>=最高分-10 等级为’A’ ;成绩>=最高分-20 等级为’B’ ;成绩>=最高分-30 等级为’C’;其余等级为’D’。
提示:先读入学生人数,根据人数创建int数组, 存放学生成绩。
import java.util.Scanner;
public class Test {
public static void main(String[] args){
Scanner input = new Scanner(System.in);
// 1、读取学生人数
System.out.print("请输入学生人数:");
int studentNum = input.nextInt();
// 2、创建数组存储学生成绩,动态初始化
int[] grades = new int[studentNum];
// 3、 给数组元素赋值
int maxGrade = 0;
for(int i=0;i<studentNum;i++){
System.out.print("请输入学生成绩:");
grades[i] = input.nextInt();
if(grades[i]>maxGrade){
maxGrade = grades[i];
}
}
// 4、获取数组中元素的最大值,即最高分
// 5、根据每个学生的成绩与最高分的差值评级
String level;
for(int i=0;i<studentNum;i++){
if(maxGrade - grades[i] <= 10){
level = "A";
}else if(maxGrade - grades[i] <= 20){
level = "B";
}else if(maxGrade - grades[i] <= 30){
level = "C";
}else{
level = "D";
}
System.out.println("student " + i + " score is " + grades[i] + " is " + level);
}
}
}
3.3 多维数组的使用
- Java 语言里提供了支持多维数组的语法。
- 如果说可以把一维数组当成几何中的线性图形,那么二维数组就相当于是一个表格,像Excel中的表格一样。
- 对于二维数组的理解,数组属于引用数据类型,数组的元素也可以是引用数据类型,一个一维数组A的元素如果是一个一维数组类型,则称数组A为二维数组。
3.3.1 二维数组的声明和初始化
静态初始化
// 静态初始化 举例
int[][] arr = new int[][]{{3,8,2},{2,7},{9,0,1,6}};
- 定义一个名称为arr的二维数组,二维数组中有三个一维数组
- 每一个一维数组中具体元素也都已初始化
- 第一个一维数组 arr[0] = {3,8,2};
- 第二个一维数组 arr[1] = {2,7};
- 第三个一维数组 arr[2] = {9,0,1,6};
- 第三个一维数组的长度表示方式:arr[2].length;
注意:
- 注意特殊写法情况:int[] x,y[]; x是一维数组,y是二维数组。
- Java中多维数组不必都是规则矩阵形式
示例:
// 静态初始化的两种形式
int[][] arr0 = new int[][]{{1,2,3}, {4,5}, {6,7,8}};
int[][] arr1 = {{1,2,3}, {4,5}, {6,7,8}}; // 类型推断
动态初始化1
// 动态初始化1 举例
int[][] arr2 = new int[3][2]; // 即两个维度的长度都定义了;
// 适用于内层数组的长度一致的情况
- 定义了名称为arr2的二维数组,最内层元素的类型为int;
- 外层数组是一个长度为3的数组,其每个元素都是长度为2、元素类型为int的一维数组;
- 外层数组的3个元素分别是长度为2的一维数组,这些一维数组的名称依次是arr[0]、arr[1]、arr[2];
- 给第一个一维数组1脚标位赋值为78写法是:
arr[0][1] = 78;
动态初始化2
// 动态初始化2 举例
int[][] arr3 = new int[3][]; // 只定义外层数组的长度
- 定义了名称为arr3的二维数组,最内层元素的类型为int;
- 外层数组是一个长度为3的数组,其每个元素都是元素类型为int的一维数组,这些内层数组的长度暂时未定义;
- 可以对内层三个一维数组分别进行初始化
arr3[0] = new int[3];
arr3[1] = new int[1];
arr3[2] = new int[2];
- 注:
int[][]arr3 = new int[][3]; //非法
3.3.2 二维数组元素的引用
int[][] arr0 = new int[][]{{1,2,3}, {4,5}, {6,7,8}};
System.out.println(arr0[0][1]); // 2
String[][] arr2 = new String[3][2];
System.out.println(arr2[0][1]); // null
String[][] arr3 = new String[3][];
System.out.println(arr3[0][1]); // 报错
arr3[0] = new String[2];
System.out.println(arr3[0][1]); // null
// 数组只有指定了长度才会分配内存,才可以进行引用
3.3.3 遍历数组
获取数组的长度
int[][] arr0 = new int[][]{{1,2,3}, {4,5}, {6,7,8}};
System.out.println(arr0.length); // 3
System.out.println(arr0[1].length); // 2
遍历二维数组
int[][] arr0 = new int[][]{{1,2,3}, {4,5}, {6,7,8}};
for(int i=0;i<arr0.length;i++){
for(int j=0;j<arr0[i].length;j++){
System.out.println(arr0[i][j]); // 打印每个元素
}
}
3.3.4 默认初始值
规定:二维数组分为外层数组的元素,内层数组的元素。外层数组元素:arr[0]、arr[1];内层数组元素:arr[0][0],arr[0][1]。
- 针对于初始化方式一,如:
String[][] arr2 = new String[3][2];
- 外层数组元素的初始化值为:地址值
- 内层数组元素的初始化值为:与一维数组一致,基本数据类型的初始化值如3.2.4节的表,引用数据类型的初始化值为null。
- 针对于初始化方式二,如
String[][] arr3 = new String[3][];
- 外层数组元素的初始化值为:null,因为外层数组的元素为数组,数组的数据类型属于引用数据类型,其初始化值为null;
- 内层数组元素的初始化值为:无。内层数组为指定长度,未分配内存空间,不能调用,否则报错,故没有数据类型。
3.3.5 内存解析
内存分配过程举例1:
左边的矩形为栈空间,右边矩形为对空间,步骤对应如黄色编号。
内存分配过程举例2:
看到倒数第二行:
System.out.print(arr[0]); // null
只要索引[0]前面的数组被分配了长度便不会报错。
最后一行:
System.out.print(arr[0][0]); // 报错
第二个索引[0]前面的数组arr[0]未定义长度,即为分配存储空间,孤儿院这种引用报错。
练习
使用二维数组打印一个 10 行杨辉三角。
【提示】
- 第一行有 1 个元素, 第 n 行有 n 个元素
- 每一行的第一个元素和最后一个元素都是 1
- 从第三行开始, 对于非第一个元素和最后一个元
素的元素。即:
yanghui[i][j] = yanghui[i-1][j-1] + yanghui[i-1][j];
public class Test {
public static void main(String[] args){
int yanghuiLength = 10;
int[][] yanghui = new int[yanghuiLength][];
for(int i=0;i<yanghui.length;i++){
yanghui[i] = new int[i+1];
for(int j=0;j<yanghui[i].length;j++){
if(j==0 || j==yanghui[i].length-1){
yanghui[i][j] = 1;
}else {
yanghui[i][j] = yanghui[i - 1][j - 1] + yanghui[i - 1][j];
}
System.out.print(yanghui[i][j] + " ");
}
System.out.println();
}
}
}
3.4 数组中涉及到的常见算法
- 数组元素的赋值(杨辉三角、回形数等)
- 求数值型数组中元素的最大值、最小值、平均数、总和等 3. 数组的复制、反转、查找(线性查找、二分法查找)
- 数组元素的排序算法
3.4.1 数组复制
(1)创建一个名为ArrayTest的类,在main()方法中声明array1和array2两个变量,他们是int[]类型的数组。
(2)使用大括号{},把array1初始化为8个素数:2,3,5,7,11,13,17,19。
(3)显示array1的内容。
(4)赋值array2变量等于array1,修改array2中的偶索引元素,使其等于索引值 (如array[0]=0,array[2]=2)。打印出array1。
思考:array1和array2是什么关系?
地址值一样,都指向了对空间中同一个的数组。
拓展:修改题目,实现array2对array1数组的复制
public class Test {
public static void main(String[] args){
int[] arr1 = new int[]{2,3,5,7,11,13,17,19};
System.out.println(arr1); //[I@4517d9a3
int[] arr2 = arr1; // 赋值一个数组等于另外一个数组
System.out.println(arr2); //[I@4517d9a3,地址相同,不能称作数组复制
for(int i=0;i<arr2.length;i+=2){
arr2[i] = i;
}
for(int i=0;i<arr1.length;i++){
System.out.print(arr1[i] + " "); // 0 3 2 7 4 13 6 19,改了arr2,arr1也变了
}
int[] arr3 = new int[arr1.length];
for(int i=0;i<arr1.length;i++){
arr3[i] = arr1[i];
}
System.out.println(arr3); //[I@2f92e0f4,地址不同,实现复制
}
}
只new过一次,所以内存中只有一个数组。
先new再赋值,复制失败:
import java.util.Arrays;
public class Test {
public static void main(String[] args){
int[] arr1 = new int[]{1,2,3,4,5};
int[] arr2 = new int[5];
System.out.println(arr1); // [I@372f7a8d
System.out.println(arr2); // [I@2f92e0f4
arr2 = arr1;
System.out.println(arr1); // [I@372f7a8d
System.out.println(arr2); // [I@372f7a8d,这里地址变成一致的了
arr2[1] = 20;
System.out.println(Arrays.toString(arr1)); // [1, 20, 3, 4, 5],arr1也改变了
System.out.println(Arrays.toString(arr2)); // [1, 20, 3, 4, 5]
}
}
3.4.2 数组反转
public class Test {
public static void main(String[] args){
String[] arr0 = new String[]{"AA","BB","CC","DD","EE","FF","GG"};
// 方式一
for(int i=0;i<arr0.length/2;i++){
String temp = arr0[i];
arr0[i] = arr0[arr0.length - 1 - i];
arr0[arr0.length - 1 - i] = temp;
}
for(int i=0;i<arr0.length;i++){
System.out.print(arr0[i] + " "); // GG FF EE DD CC BB AA
}
System.out.println();
// 方式二
for(int i=0,j=arr0.length-1;i<j;i++,j--){
String temp = arr0[i];
arr0[i] = arr0[j];
arr0[j] = temp;
}
for(int i=0;i<arr0.length;i++){
System.out.print(arr0[i] + " "); // GG FF EE DD CC BB AA
}
}
}
3.4.3 数组查找
遍历查找,从头到尾遍历一遍查找
public class Test {
public static void main(String[] args){
// 遍历查找
String[] arr0 = new String[]{"AA","BB","CC","DD","EE","FF","GG"};
String dest = "FF";
boolean flag = false;
for(int i=0;i<arr0.length;i++){
if(arr0[i] == dest){
System.out.println(dest + "是数组arr0的第" + (i+1) + "个元素。");
flag = true;
break;
}
}
if(!flag){
System.out.println(dest + "不是数组arr0的元素。");
}
}
}
不用flag的遍历查找:
public class Test {
public static void main(String[] args){
// 遍历查找
String[] arr0 = new String[]{"AA","BB","CC","DD","EE","FF","GG"};
String dest = "FFE";
// boolean flag = false;
int i=0; // 保证下面for循环外的if语句可以访问到i
for(;i<arr0.length;i++){ // 初始化置空
if(arr0[i] == dest){
System.out.println(dest + "是数组arr0的第" + (i+1) + "个元素。");
// flag = true;
break;
}
}
if(i == arr0.length){ // 能访问i。为什么不把该判断放for循环里面:费时
System.out.println(dest + "不是数组arr0的元素。");
}
}
}
二分法查找
步骤:
- 二分法前提:数组有序;
- 初始化左索引left为最小索引,右索引right为最大索引;
- 循环:
(1)循环条件为:左索引left小于或等于右索引right;
(2)索引mid为左索引和右索引的截断均值;
(3)判断mid对应的元素是否为要查找的目标,若是则已找到目标,跳出循环;若否,则继续;
(4)若mid对应值大于目标,则right = mid - 1;若mid对应值小于目标,则left = mid + 1;
public class Test {
public static void main(String[] args){4
// 二分法查找
// 该方法适用于有序数组
int dest1 = 79;
int[] arr1 = new int[]{2,3,6,7,9,23,35,36,45,56,57,67,73,79,85,89,90};
int left=0, right=arr1.length-1;
int mid;
boolean flag1 = false;
while(left<=right){
mid = (left+right) / 2;
if(arr1[mid] == dest1){
System.out.println(dest1 + "是数组arr1的第" + (mid+1) + "个元素。");
flag1 = true;
break;
}else if(arr1[mid] > dest1){
right = mid-1;
}else if(arr1[mid] < dest1){
left = mid+1;
}
}
if(!flag1){
System.out.println(dest1 + "不是数组arr1的元素。");
}
}
}
3.4.4 排序算法
排序算法参考:https://www.runoob.com/w3cnote/ten-sorting-algorithm.html
排序:假设含有n个记录的序列为{R1,R2,…,Rn},其相应的关键字序列为{K1,K2,…,Kn}。将这些记录重新排序为{Ri1,Ri2,…,Rin},使得相应的关键字值满足条Ki1<=Ki2<=…<=Kin,这样的一种操作称为排序。
通常来说,排序的目的是快速查找。
衡量排序算法的优劣:
- 时间复杂度:分析关键字的比较次数和记录的移动次数
- 空间复杂度:分析排序算法中需要多少辅助内存
- 稳定性:若两个记录A和B的关键字值相等,但排序后A、B的先后次序保 持不变,则称这种排序算法是稳定的。
排序算法分类:内部排序和外部排序。
- 内部排序:整个排序过程不需要借助于外部存储器(如磁盘等),所有排序操作都在内存中完成。
- 外部排序:参与排序的数据非常多,数据量非常大,计算机无法把整个排 序过程放在内存中完成,必须借助于外部存储器(如磁盘)。外部排序最 常见的是多路归并排序。可以认为外部排序是由多次内部排序组成。
十大内部排序算法:
- 选择排序
直接选择排序、堆排序(懂思想) - 交换排序
冒泡排序、快速排序(这俩要会) - 插入排序
直接插入排序、折半插入排序、Shell排序 - 归并排序(懂思想)
- 桶式排序
- 基数排序
算法的5大特征
说明:满足确定性的算法也称为:确定性算法。现在人们也关注更广泛的概念,例如考虑各种非确定性的算法,如并行算法、概率算法等。另外,人们也关注并不要求终止的计算描述,这种描述有时被称为过程(procedure)。
3.4.4.1 冒泡排序
介绍:
冒泡排序的原理非常简单,它重复地走访过要排序的数列,一次比较两个元
素,如果他们的顺序错误就把他们交换过来。
排序思想:
- 比较相邻的元素。如果第一个比第二个大(降序),就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较为止。
public class Test {
public static void main(String[] args){
int[] arr = new int[]{43, 32, 76, -98, 0, 64, 33, -21, 32, 99};
// 冒泡排序
int temp;
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]){
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
for(int i=0;i<arr.length;i++){
System.out.print(arr[i] + " ");
}
}
}
3.4.4.2 快速排序
介绍:
快速排序通常明显比同为O(nlogn)的其他算法更快,因此常被采用,而且快排采用了分治法的思想,所以在很多笔试面试中能经常看到快排的影子。可见掌握快排的重要性。
快速排序(Quick Sort)由图灵奖获得者Tony Hoare发明,被列为20世纪十大算法之一,是迄今为止所有内排序算法中速度最快的一种。冒泡排序的升级版,交换排序的一种。快速排序的时间复杂度为O(nlog(n))。
排序思想:
- 从数列中挑出一个元素,称为"基准"(pivot),
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
- 递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
3.4.4.3 排序算法性能对比
冒泡排序,n方;快速排序nlogn。
各种内部排序方法性能比较:
- 从平均时间而言:快速排序最佳。但在最坏情况下时间性能不如堆排序和归并排序。
- 从算法简单性看:由于直接选择排序、直接插入排序和冒泡排序的算法比较简单,将其认为是简单算法。对于Shell排序、堆排序、快速排序和归并排序算法,其算法比较复杂,认为是复杂排序。
- 从稳定性看:直接插入排序、冒泡排序和归并排序时稳定的;而直接选择排序、快速排序、 Shell排序和堆排序是不稳定排序
- 从待排序的记录数n的大小看,n较小时,宜采用简单排序;而n较大时宜采用改进排序。
排序算法的选择:
- 若n较小(如n≤50),可采用直接插入或直接选择排序。当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插入,应选直接选择排序为宜。
- 若文件初始状态基本有序(指正序),则应选用直接插入、冒泡或随机的快速排序为宜;
- 若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
3.5 Arrays工具类的使用
java.util.Arrays类即为操作数组的工具类,包含了用来操作数组(比如排序和搜索)的各种方法。
方法名 | 作用 |
---|---|
boolean equals(int[] a,int[] b) | 判断两个数组是否相等。 |
String toString(int[] a) | 输出数组信息。 |
void fill(int[] a,int val) | 将指定值填充到数组之中,数组中所有值都将为val。 |
void sort(int[] a) | 对数组进行排序。 |
int binarySearch(int[] a,int key) | 对排序后的数组进行二分法检索指定的值。若未找到,返回负值。 |
import java.util.Arrays;
public class Test {
public static void main(String[] args){
// boolean equals(int[] a,int[] b)
int[] arr1 = new int[]{1,2,3,4};
int[]arr2 = {1,4,3,2};
boolean isEqual = Arrays.equals(arr1,arr2);
System.out.println(isEqual);
// String toString(int[] a)
System.out.println(Arrays.toString(arr2));
// void fill(int[] a,int val
Arrays.fill(arr1,10);
System.out.println(Arrays.toString(arr1));
// void sort(int[] a)
Arrays.sort(arr2);
System.out.println(Arrays.toString(arr2));
// int binarySearch(int[] a,int key)
int[] arr3 = new int[]{43, 32, 76, -98, 0, 64, 33, -21, 32, 99};
int index = Arrays.binarySearch(arr3,100);
if(index>=0){
System.out.println(index);
}else{
System.out.println("未找到 : " + index + " : " +arr3.length);
}
}
}
3.6 数组使用中的常见异常
3.6.1 数组脚标越界异常
(ArrayIndexOutOfBoundsException)
int[] arr = new int[2]; // 2为length
System.out.println(arr[2]); // 报错,只能访问0、1
System.out.println(arr[-1]); // 访问到了数组中的不存在的脚标时发生。
// Java的数组索引只能:0 --> arr.length-1
3.6.2 空指针异常
(NullPointerException)
int[] arr = null;
System.out.println(arr[0]); //arr引用没有指向实体,却在操作实体中的元素时。
// 该报错可能是因为引用元素没有具体赋值
视频网址:https://www.bilibili.com/video/BV1Kb411W75N?p=1
课程资料:http://www.atguigu.com/download_detail.shtml?v=129