跳到主要内容

数组

数组

概念

数组(Array),是多个相同类型数据按一定顺序排列的集合,并使用一个名字命名,并通过编号的方式对这些数据进行统一管理

数组中的概念

  • 数组名
  • 下标(或索引)
  • 元素
  • 数组的长度

20240131163141

数组的特点:

  • 数组本身是引用数据类型,而数组中的元素可以是任何数据类型,包括基本数据类型和引用数据类型
  • 创建数组对象会在内存中开辟一整块连续的空间。占据的空间的大小,取决于数组的长度和数组中元素的类型
  • 数组中的元素在内存中是依次紧密排列的,有序的
  • 数组,一旦初始化完成,其长度就是确定的。数组的长度一旦确定,就不能修改
  • 可以直接通过下标(或索引)的方式调用指定位置的元素,速度很快
  • 数组名中引用的是这块连续空间的首地址

分类

1、按照元素类型分:

  • 基本数据类型元素的数组:每个元素位置存储基本数据类型的值
  • 引用数据类型元素的数组:每个元素位置存储对象(本质是存储对象的首地址)(在面向对象部分讲解)

2、按照维度分:

  • 一维数组:存储一组数据
  • 二维数组:存储多组数据,相当于二维表,一行代表一组数据,只是这里的二维表每一行长度不要求一样

20240131163420

一维数据

声明

格式:

//推荐
元素的数据类型[] 一维数组的名称;

//不推荐
元素的数据类型 一维数组名[];

举例:

int[] arr;
int arr1[];
double[] arr2;
String[] arr3; //引用类型变量数组

数组的声明,需要明确:

(1)数组的维度:在Java中数组的符号是[][]表示一维,[][]表示二维

(2)数组的元素类型:即创建的数组容器可以存储什么数据类型的数据。元素的类型可以是任意的 Java 的数据类型。例如:int、String、Student 等

(3)数组名:就是代表某个数组的标识符,数组名其实也是变量名,按照变量的命名规范来命名。数组名是个引用数据类型的变量,因为它代表一组数据

声明

静态初始化

如果数组变量的初始化和数组元素的赋值操作同时进行,那就称为静态初始化

静态初始化,本质是用静态数据(编译时已知)为数组初始化。此时数组的长度由静态数据的个数决定

一维数组声明和静态初始化格式1

数据类型[] 数组名 = new 数据类型[]{元素1,元素2,元素3,...};



数据类型[] 数组名;
数组名 = new 数据类型[]{元素1,元素2,元素3,...};

new:关键字,创建数组使用的关键字。因为数组本身是引用数据类型,所以要用 new 创建数组实体

例如,定义存储1,2,3,4,5整数的数组容器

int[] arr = new int[]{1,2,3,4,5};//正确
//或
int[] arr;
arr = new int[]{1,2,3,4,5};//正确

一维数组声明和静态初始化格式2

数据类型[] 数组名 = {元素1,元素2,元素3...};//必须在一个语句中完成,不能分成两个语句写

如,定义存储1,2,3,4,5整数的数组容器

int[] arr = {1,2,3,4,5};//正确

int[] arr;
arr = {1,2,3,4,5};//错误

举例

public class ArrayTest2 {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};//右边不需要写new int[]

int[] nums;
nums = new int[]{10,20,30,40}; //声明和初始化在两个语句完成,就不能使用new int[]

char[] word = {'h','e','l','l','o'};

String[] heros = {"袁隆平","邓稼先","钱学森"};

System.out.println("arr数组:" + arr);//arr数组:[I@1b6d3586
System.out.println("nums数组:" + nums);//nums数组:[I@4554617c
System.out.println("word数组:" + word);//word数组:[C@74a14482
System.out.println("heros数组:" + heros);//heros数组:[Ljava.lang.String;@1540e19d
}
}

动态初始化

数组变量的初始化和数组元素的赋值操作分开进行,即为动态初始化

动态初始化中,只确定了元素的个数(即数组的长度),而元素值此时只是默认值,还并未真正赋自己期望的值。真正期望的数据需要后续单独一个一个赋值

格式

数组存储的元素的数据类型[] 数组名字 = new 数组存储的元素的数据类型[长度];



数组存储的数据类型[] 数组名字;
数组名字 = new 数组存储的数据类型[长度];
  • [长度]:数组的长度,表示数组容器中可以最多存储多少个元素
  • **注意:数组有定长特性,长度一旦指定,不可更改。**和水杯道理相同,买了一个2升的水杯,总容量就是2升是固定的 正确写法
int[] arr = new int[5];

int[] arr;
arr = new int[5];

使用

  • 数组的元素总个数,即数组的长度
  • 每个数组都有一个属性 length 指明它的长度,例如:arr.length 指明数组 arr 的长度(即元素个数)
  • 每个数组都具有长度,而且一旦初始化,其长度就是确定,且是不可变的

如何表示数组中的一个元素?

每一个存储到数组的元素,都会自动的拥有一个编号,从0开始,这个自动编号称为数组索引(index)或下标,可以通过数组的索引/下标访问到数组中的元素

数组名[索引/下标]

数组的下标范围?

Java 中数组的下标从[0]开始,下标范围是[0, 数组的长度-1],即[0, 数组名.length-1]

遍历

将数组中的每个元素分别获取出来,就是遍历。for 循环与数组的遍历是绝配

public class ArrayTest4 {
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4,5};
//打印数组的属性,输出结果是5
System.out.println("数组的长度:" + arr.length);

//遍历输出数组中的元素
System.out.println("数组的元素有:");
for(int i=0; i<arr.length; i++){
System.out.println(arr[i]);
}
}
}

默认值

数组是引用类型,当使用动态初始化方式创建数组时,元素值只是默认值。例如:

public class ArrayTest6 {
public static void main(String argv[]){
int a[]= new int[5];
System.out.println(a[3]); //a[3]的默认值为0
}
}

对于基本数据类型而言,默认初始化值各有不同

对于引用数据类型而言,默认初始化值为 null(注意与0不同!)

20240131165439

一维数组内存分析

Java虚拟机的内存划分

为了提高运算效率,就对空间进行了不同区域的划分,因为每一片区域都有特定的处理数据方式和内存管理方式

20240131165538

区域名称作用
虚拟机栈用于存储正在执行的每个 Java 方法的局部变量表等。局部变量表存放了编译期可知长度的各种基本数据类型、对象引用,方法执行完,自动释放。
堆内存存储对象(包括数组对象),new 来创建的,都存储在堆内存。
方法区存储已被虚拟机加载的类信息、常量、(静态变量)、即时编译器编译后的代码等数据。
本地方法栈当程序中调用了 native 的本地方法时,本地方法执行期间的内存区域
程序计数器程序计数器是 CPU 中的寄存器,它包含每一个线程下一条要执行的指令的地址

一维数组在内存中的存储

一个一维数组内存图

public static void main(String[] args) {
int[] arr = new int[3];
System.out.println(arr);//[I@5f150435
}

20240131165750

数组下标为什么是从0开始

因为第一个元素距离数组首地址间隔0个单元格

两个一维数组内存图

两个数组相互独立

public static void main(String[] args) {
int[] arr = new int[3];
int[] arr2 = new int[2];
System.out.println(arr);
System.out.println(arr2);
}

20240131165844

两个变量指向一个一维数组

两个数组变量本质上代表同一个数组

public static void main(String[] args) {
// 定义数组,存储3个元素
int[] arr = new int[3];
//数组索引进行赋值
arr[0] = 5;
arr[1] = 6;
arr[2] = 7;
//输出3个索引上的元素值
System.out.println(arr[0]);
System.out.println(arr[1]);
System.out.println(arr[2]);
//定义数组变量arr2,将arr的地址赋值给arr2
int[] arr2 = arr;
arr2[1] = 9;
System.out.println(arr[1]);
}

20240131165927

多维数组

概述

Java 语言里提供了支持多维数组的语法

声明与初始化

二维数组声明的语法格式:

//推荐
元素的数据类型[][] 二维数组的名称;

//不推荐
元素的数据类型 二维数组名[][];
//不推荐
元素的数据类型[] 二维数组名[];

例如

public class Test20TwoDimensionalArrayDefine {
public static void main(String[] args) {
//存储多组成绩
int[][] grades;

//存储多组姓名
String[][] names;
}
}

静态初始化

格式

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是二维数组

动态初始化

如果二维数组的每一个数据,甚至是每一行的列数,需要后期单独确定,那么就只能使用动态初始化方式了。动态初始化方式分为两种格式:

格式1:规则二维表:每一行的列数是相同的

//(1)确定行数和列数
元素的数据类型[][] 二维数组名 = new 元素的数据类型[m][n];
//其中,m:表示这个二维数组有多少个一维数组。或者说一共二维表有几行
//其中,n:表示每一个一维数组的元素有多少个。或者说每一行共有一个单元格

//此时创建完数组,行数、列数确定,而且元素也都有默认值

//(2)再为元素赋新值
二维数组名[行下标][列下标] =;

举例:

int[][] arr = new int[3][2];

  • 定义了名称为 arr 的二维数组
  • 二维数组中有3个一维数组
  • 每一个一维数组中有2个元素
  • 一维数组的名称分别为arr[0], arr[1], arr[2]
  • 给第一个一维数组1脚标位赋值为78写法是:arr[0][1] = 78;

格式2:不规则:每一行的列数不一样

//(1)先确定总行数
元素的数据类型[][] 二维数组名 = new 元素的数据类型[总行数][];

//此时只是确定了总行数,每一行里面现在是null

//(2)再确定每一行的列数,创建每一行的一维数组
二维数组名[行下标] = new 元素的数据类型[该行的总列数];

//此时已经new完的行的元素就有默认值了,没有new的行还是null

//(3)再为元素赋值
二维数组名[行下标][列下标] =;

举例:

int[][] arr = new int[3][];

  • 二维数组中有3个一维数组
  • 每个一维数组都是默认初始化值 null (注意:区别于格式1)
  • 可以对这个三个一维数组分别进行初始化:arr[0] = new int[3]; arr[1] = new int[1]; arr[2] = new int[2];
  • 注:int[][]arr = new int[][3]; //非法

数组长度和角标

  • 二维数组的长度/行数:二维数组名.length
  • 二维数组的某一行:二维数组名[行下标],此时相当于获取其中一组数据。它本质上是一个一维数组。行下标的范围:[0, 二维数组名.length-1]。此时把二维数组看成一维数组的话,元素是行对象
  • 某一行的列数:二维数组名[行下标].length,因为二维数组的每一行是一个一维数组
  • 某一个元素:二维数组名[行下标][列下标],即先确定行/组,再确定列

二维数组的遍历

格式:

for(int i=0; i<二维数组名.length; i++){ //二维数组对象.length
for(int j=0; j<二维数组名[i].length; j++){//二维数组行对象.length
System.out.print(二维数组名[i][j]);
}
System.out.println();
}

内存解析

二维数组本质上是元素类型是一维数组的一维数组

int[][] arr = {
{1},
{2,2},
{3,3,3},
{4,4,4,4},
{5,5,5,5,5}
};

20240131171528

//1、声明二维数组,并确定行数和列数
int[][] arr = new int[4][5];

//2、确定元素的值
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length; j++) {
arr[i][j] = i + 1;
}
}

20240131171544

//1、声明一个二维数组,并且确定行数
//因为每一行的列数不同,这里无法直接确定列数
int[][] arr = new int[5][];

//2、确定每一行的列数
for(int i=0; i<arr.length; i++){
/*
arr[0] 的列数是1
arr[1] 的列数是2
arr[2] 的列数是3
arr[3] 的列数是4
arr[4] 的列数是5
*/
arr[i] = new int[i+1];
}

//3、确定元素的值
for(int i=0; i<arr.length; i++){
for(int j=0; j<arr[i].length; j++){
arr[i][j] = i+1;
}
}

20240131171558

数组常见算法

数组元素的反转

**实现思想:**数组对称位置的元素互换

20240131171641

public class TestArrayReverse1 {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
System.out.println("反转之前:");
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}

//反转
/*
思路:首尾对应位置的元素交换
(1)确定交换几次
次数 = 数组.length / 2
(2)谁和谁交换
for(int i=0; i<次数; i++){
int temp = arr[i];
arr[i] = arr[arr.length-1-i];
arr[arr.length-1-i] = temp;
}
*/
for(int i=0; i<arr.length/2; i++){
int temp = arr[i];
arr[i] = arr[arr.length-1-i];
arr[arr.length-1-i] = temp;
}

System.out.println("反转之后:");
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}

}

数组的扩容与缩容

题目:现有数组 int[] arr = new int[]{1,2,3,4,5}; ,现将数组长度扩容1倍,并将10,20,30三个数据添加到arr数组中,如何操作?

public class ArrTest1 {
public static void main(String[] args) {

int[] arr = new int[]{1,2,3,4,5};
int[] newArr = new int[arr.length << 1];

for(int i = 0;i < arr.length;i++){
newArr[i] = arr[i];
}

newArr[arr.length] = 10;
newArr[arr.length + 1] = 20;
newArr[arr.length + 2] = 30;

arr = newArr;

//遍历arr
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}

题目:现有数组 int[] arr={1,2,3,4,5,6,7}。现需删除数组中索引为4的元素

public class ArrTest2 {
public static void main(String[] args) {

int[] arr = {1, 2, 3, 4, 5, 6, 7};
//删除数组中索引为4的元素
int delIndex = 4;
//方案1:
/*//创建新数组
int[] newArr = new int[arr.length - 1];

for (int i = 0; i < delIndex; i++) {
newArr[i] = arr[i];
}
for (int i = delIndex + 1; i < arr.length; i++) {
newArr[i - 1] = arr[i];
}

arr = newArr;
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}*/

//方案2:
for (int i = delIndex; i < arr.length - 1; i++) {
arr[i] = arr[i + 1];
}
arr[arr.length - 1] = 0;

for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}

数组元素的查找

顺序查找

public class TestArrayOrderSearch {
//查找value第一次在数组中出现的index
public static void main(String[] args){
int[] arr = {4,5,6,1,9};
int value = 1;
int index = -1;

for(int i=0; i<arr.length; i++){
if(arr[i] == value){
index = i;
break;
}
}

if(index==-1){
System.out.println(value + "不存在");
}else{
System.out.println(value + "的下标是" + index);
}
}
}

二分查找

20240131171810

20240131171818

//二分法查找:要求此数组必须是有序的。
int[] arr3 = new int[]{-99,-54,-2,0,2,33,43,256,999};
boolean isFlag = true;
int value = 256;
//int value = 25;
int head = 0;//首索引位置
int end = arr3.length - 1;//尾索引位置
while(head <= end){
int middle = (head + end) / 2;
if(arr3[middle] == value){
System.out.println("找到指定的元素,索引为:" + middle);
isFlag = false;
break;
}else if(arr3[middle] > value){
end = middle - 1;
}else{//arr3[middle] < value
head = middle + 1;
}
}

if(isFlag){
System.out.println("未找打指定的元素");
}

数组元素排序

冒泡排序

排序思想:

  1. 比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数
  3. 针对所有的元素重复以上的步骤,除了最后一个
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较为止

20240131171940

/*
1、冒泡排序(最经典)
思想:每一次比较“相邻(位置相邻)”元素,如果它们不符合目标顺序(例如:从小到大),
就交换它们,经过多轮比较,最终实现排序。
(例如:从小到大) 每一轮可以把最大的沉底,或最小的冒顶。

过程:arr{6,9,2,9,1} 目标:从小到大

第一轮:
第1次,arr[0]与arr[1],6>9不成立,满足目标要求,不交换
第2次,arr[1]与arr[2],9>2成立,不满足目标要求,交换arr[1]与arr[2] {6,2,9,9,1}
第3次,arr[2]与arr[3],9>9不成立,满足目标要求,不交换
第4次,arr[3]与arr[4],9>1成立,不满足目标要求,交换arr[3]与arr[4] {6,2,9,1,9}
第一轮所有元素{6,9,2,9,1}已经都参与了比较,结束。
第一轮的结果:第“一”最大值9沉底(本次是后面的9沉底),即到{6,2,9,1,9}元素的最右边

第二轮:
第1次,arr[0]与arr[1],6>2成立,不满足目标要求,交换arr[0]与arr[1] {2,6,9,1,9}
第2次,arr[1]与arr[2],6>9不成立,满足目标要求,不交换
第3次:arr[2]与arr[3],9>1成立,不满足目标要求,交换arr[2]与arr[3] {2,6,1,9,9}
第二轮未排序的所有元素 {6,2,9,1}已经都参与了比较,结束。
第二轮的结果:第“二”最大值9沉底(本次是前面的9沉底),即到{2,6,1,9}元素的最右边
第三轮:
第1次,arr[0]与arr[1],2>6不成立,满足目标要求,不交换
第2次,arr[1]与arr[2],6>1成立,不满足目标要求,交换arr[1]与arr[2] {2,1,6,9,9}
第三轮未排序的所有元素{2,6,1}已经都参与了比较,结束。
第三轮的结果:第三最大值6沉底,即到 {2,1,6}元素的最右边
第四轮:
第1次,arr[0]与arr[1],2>1成立,不满足目标要求,交换arr[0]与arr[1] {1,2,6,9,9}
第四轮未排序的所有元素{2,1}已经都参与了比较,结束。
第四轮的结果:第四最大值2沉底,即到{1,2}元素的最右边

*/
public class Test19BubbleSort{
public static void main(String[] args){
int[] arr = {6,9,2,9,1};

//目标:从小到大
//冒泡排序的轮数 = 元素的总个数 - 1
//轮数是多轮,每一轮比较的次数是多次,需要用到双重循环,即循环嵌套
//外循环控制 轮数,内循环控制每一轮的比较次数和过程
for(int i=1; i<arr.length; i++){ //循环次数是arr.length-1次/轮
/*
假设arr.length=5
i=1,第1轮,比较4次
arr[0]与arr[1]
arr[1]与arr[2]
arr[2]与arr[3]
arr[3]与arr[4]

arr[j]与arr[j+1],int j=0;j<4; j++

i=2,第2轮,比较3次
arr[0]与arr[1]
arr[1]与arr[2]
arr[2]与arr[3]

arr[j]与arr[j+1],int j=0;j<3; j++

i=3,第3轮,比较2次
arr[0]与arr[1]
arr[1]与arr[2]

arr[j]与arr[j+1],int j=0;j<2; j++
i=4,第4轮,比较1次
arr[0]与arr[1]

arr[j]与arr[j+1],int j=0;j<1; j++

int j=0; j<arr.length-i; j++
*/
for(int j=0; j<arr.length-i; j++){
//希望的是arr[j] < arr[j+1]
if(arr[j] > arr[j+1]){
//交换arr[j]与arr[j+1]
int 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]+" ");
}
}
}

快速排序

快速排序(Quick Sort)由图灵奖获得者Tony Hoare发明,被列为20世纪十大算法之一,是迄今为止所有内排序算法中速度最快的一种,快速排序的时间复杂度为O(nlog(n))

快速排序通常明显比同为O(nlogn)的其他算法更快,因此常被采用,而且快排采用了分治法的思想,所以在很多笔试面试中能经常看到快排的影子

排序思想:

  1. 从数列中挑出一个元素,称为"基准"(pivot)
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序
  4. 递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去

Arrays工具类的使用

java.util.Arrays类即为操作数组的工具类,包含了用来操作数组(比如排序和搜索)的各种方法。 比如:

  • 数组元素拼接
    • static String toString(int[] a) :字符串表示形式由数组的元素列表组成,括在方括号("[]")中。相邻元素用字符 ", "(逗号加空格)分隔。形式为:[元素1,元素2,元素3...]
    • static String toString(Object[] a) :字符串表示形式由数组的元素列表组成,括在方括号("[]")中。相邻元素用字符 ", "(逗号加空格)分隔。元素将自动调用自己从 Object 继承的 toString 方法将对象转为字符串进行拼接,如果没有重写,则返回类型@hash值,如果重写则按重写返回的字符串进行拼接。
  • 数组排序
    • static void sort(int[] a) :将a数组按照从小到大进行排序
    • static void sort(int[] a, int fromIndex, int toIndex) :将a数组的[fromIndex, toIndex)部分按照升序排列
    • static void sort(Object[] a) :根据元素的自然顺序对指定对象数组按升序进行排序。
    • static <T> void sort(T[] a, Comparator<? super T> c) :根据指定比较器产生的顺序对指定对象数组进行排序。
  • 数组元素的二分查找
    • static int binarySearch(int[] a, int key) static int binarySearch(Object[] a, Object key) :要求数组有序,在数组中查找 key 是否存在,如果存在返回第一次找到的下标,不存在返回负数。
  • 数组的复制
    • static int[] copyOf(int[] original, int newLength) :根据 original 原数组复制一个长度为 newLength 的新数组,并返回新数组
    • static <T> T[] copyOf(T[] original,int newLength):根据 original 原数组复制一个长度为 newLength 的新数组,并返回新数组
    • static int[] copyOfRange(int[] original, int from, int to) :复制 original 原数组的[from,to)构成新数组,并返回新数组
    • static <T> T[] copyOfRange(T[] original,int from,int to):复制 original 原数组的[from,to)构成新数组,并返回新数组
  • 比较两个数组是否相等
    • static boolean equals(int[] a, int[] a2) :比较两个数组的长度、元素是否完全相同
    • static boolean equals(Object[] a,Object[] a2):比较两个数组的长度、元素是否完全相同
  • 填充数组
    • static void fill(int[] a, int val) :用 val 值填充整个 a 数组
    • static void fill(Object[] a,Object val):用 val 对象填充整个 a 数组
    • static void fill(int[] a, int fromIndex, int toIndex, int val):将 a 数组[fromIndex,toIndex)部分填充为val值
    • static void fill(Object[] a, int fromIndex, int toIndex, Object val):将 a 数组[fromIndex,toIndex)部分填充为val对象

举例:java.util.Arrays类的sort()方法提供了数组元素排序功能:

import java.util.Arrays;
public class SortTest {
public static void main(String[] args) {
int[] arr = {3, 2, 5, 1, 6};
System.out.println("排序前" + Arrays.toString(arr));
Arrays.sort(arr);
System.out.println("排序后" + Arrays.toString(arr));
}
}