【Java】数组的定义与使用(万字详解!) + 冒泡排序、二分查找、数组拷贝等典型例题 + 二维数组 —— 有码有图有真相

目录

数组的定义与使用

1. 数组的基本概念

1.1 为什么要使用数组

1.2 什么是数组

1.3 数组的创建及初始化

1.3.1 数组的创建

1.3.2 数组的初始化 — 两种方式

1.3.3 数组的使用

2. 数组是引用类型

2.1 初始JVM的内存分布

2.2 基本类型变量与引用类型变量的区别

2.3 认识null

3. 作为函数参数传递

3.1 参数传基本数据类型

3.2 参数传数组类型(引用数据类型,重点图解)

3.3 引用中常遇见的问题

4. 数组作为方法的返回值

5. 关于数组经典例题

5.1 数组转字符串

5.2 数组的拷贝

1、模拟实现数组的拷贝

2、用copyOf方法进行拷贝

3、直接用System.arraycopy方法进行拷贝

4、通过数组名调用clone方法 — 克隆

5.3 查找数组中指定元素(顺序、二分查找)

5.4 数组排序(冒泡排序)

5.5 数组的逆序

5.6 数组数字(奇偶)排列

6. 二维数组

6.1 基本概念

6.2 二维数组打印方式

6.3 不规则二维数组


数组的定义与使用

1. 数组的基本概念

1.1 为什么要使用数组

假设现在要存5个学生的javaSE考试成绩,并对其进行输出,按照之前掌握的知识点,我么会写出如下代码:

public class TestStudent {
    public static void main(String[] args) {
        int score1 = 70;
        int score2 = 80;
        int score3 = 85;
        int score4 = 60;
        int score5 = 90;
        System.out.println(score1);
        System.out.println(score2);
        System.out.println(score3);
        System.out.println(score4);
        System.out.println(score5);
    }
}

上述代码没有任何问题,但不好的是:如果有20名同学成绩呢,需要创建20个变量吗?有100个学生的成绩那不得要创建100个变量。仔细观察这些学生成绩发现:所有成绩的类型都是相同的,那Java中存在可以存储相同类型多个数据的类型吗?这就是本篇博客要讲的数组。

1.2 什么是数组

数组:可以看成是相同类型元素的一个集合。在内存中是一段连续的空间。比如现实中的车库:

在java中,包含6个整型类型元素的数组,就相当于上图中连在一起的6个车位,从上图中可以看到:

  1. 数组中存放的元素类型相同
  2. 数组的空间是连在一起的
  3. 每个空间有自己的编号,起始位置的编号为0,即数组的下标

数组本质上就是让我们能“批量”创建相同类型的变量。

1.3 数组的创建及初始化

1.3.1 数组的创建

T[ ] 数组名 = new T[N];

  • T:表示数组中存放元素的类型
  • T[ ]表示数组的类型
  • N:表示数组的长度

new 是关键字,一般是用来new对象的,数组就是一个对象(Java中一切皆对象)

引用类型变量的创建都需要new出来

int[] array1 = new int[10]; // 创建一个可以容纳10个int类型元素的数组
double[] array2 = new double[5]; // 创建一个可以容纳5个double类型元素的数组
String[] array3 = new String[3]; // 创建一个可以容纳3个字符串元素的数组

【注意事项】

  • java中数组的数据类型是T[ ],而不是T[n],而在C语言中,数组的数据类型是T[n]。

比如:数组中存放3个整型元素,“int[] arr = new int[3]”,那么该数组arr的数据类型是int[ ],而不是int[3]。

  • 数组也可以按照C语言方式创建:int array1[ ] = new int[10]; 不推荐。
  • 错误的将数组长度定义写在第一个方括号[ ]里面。

1.3.2 数组的初始化 — 两种方式

数组的初始化主要分为动态初始化静态初始化

1.动态初始化(完全默认初始化)

在创建数组时,直接指定数组中元素的个数。创建后,从0到N-1的数组元素都被默认初始化

语法格式:数据类型[ ] 数据名称 = new 数据类型 [ ]

例:int[ ] array = new int[10];    10:表示数组长度

  • 如果数组中存储元素类型为基本数据类型,默认值为基本数据类型对应的默认值,比如:
类型默认值
byte0
short0
int0
long0
float0.0f
double0.0
char\u0000
booleanfalse
  • \u 是用来表示Unicode转义字符的前缀。它的格式为 \u 后面跟着四个十六进制数字,用于表示一个特定的Unicode字符。

例如:\u0041 表示字符 A,因为 0041 是 A 的Unicode编码。

  • 如果数组中存储元素类型为引用类型,默认值为null

2. 静态初始化

在创建数组时不直接指定数据元素个数,而直接将具体的数据内容进行指定

(完全格式)语法格式①:T[ ] 数组名 = new T[ ]{data1, data2, data3, ..., datan}; 

(省略格式)语法格式②:T[ ] 数组名 = {data1, data2, data3, ..., datan};  

例如:

//格式1的静态初始化:
int[] array1 = new int[]{1,2,3,4,5};
double[] array2 = new double[]{1.0, 2.0, 3.0, 4.0, 5.0};
String[] array3 = new String[]{"hell", "Java", "!!!"};


//格式2的静态初始化:
int[] array1 = {1,2,3,4,5};
double[] array2 = {1.0, 2.0, 3.0, 4.0, 5.0};
String[] array3 = {"hell", "Java", "!!!"};

图示:

静态初始化虽然没有指定数组的长度,编译器在编译时会根据{ }中元素个数来确定数组的长度。

静态初始化时,{}中数据类型必须与[]前数据类型一致。

静态初始化可以简写,省去后面的new T[ ](虽然省去了new T[ ],但是编译器编译代码时还是会还原)

注意事项:

  • 数组也可以按照如C语言方式创建,int arr[] = {1, 2, 3}; 不推荐(该种定义方式不太友好,容易造成数组的类型就是int的误解。[ ]如果在类型之后,就表示数组类型,因此int[ ]结合在一块写意思更清晰)
  • 静态和动态初始化可以分为两步,但是省略格式不可以(省略格式数组进行整体赋值的时候,只有一次机会,那就是在定义的时候
int[] array1;
array1 = new int[10];
int[] array2;
array2 = new int[]{10, 20, 30};
// 注意省略格式不可以拆分, 否则编译失败
// int[] array3;
// array3 = {1, 2, 3};
  • 在使用静态初始化时浮点数数组不能用float[ ]类型接收,整数无限制

  • 数组的初始化不能既是动态初始化,又是静态初始化(说明:Java中不能像C语言那样不完全初始化)

这里既直接指定了数组的大小是10(动态初始化),又用大括号整体给数组赋值(静态初始化),所以报错了。

1.3.3 数组的使用

数组中元素访问 & 数组下标越界 & 获取的数组长度 

数组在内存中是一段连续的空间,空间的编号都是从0开始的,依次递增,该编号称为数组的下标数组可以通过下标访问其任意位置的元素

int[]array = new int[]{10, 20, 30, 40, 50};
System.out.println(array[0]);
System.out.println(array[1]);
System.out.println(array[2]);
System.out.println(array[3]);
System.out.println(array[4]);
// 也可以通过[]对数组中的元素进行修改
array[0] = 100;
System.out.println(array[0]);

//运行结果
10
20
30
40
50
100

【注意事项】

  1. 数组是一段连续的内存空间,因此支持随机访问,即通过下标访问快速访问数组中任意位置的元素(使用[ ] 操作技能读取数据,也能修改数据)
  2. 下标从0开始,介于[0, N)之间不包含N,N为元素个数,
  3. 使用 数组对象.length 能够获取到数组的长度,. 这个操作为成员访问操作符。后面在面向对象中会经常用到。
  4. 下标访问操作不能超出有效范围 [0, length - 1],如果超出有效范围,会出现数组下标越界异常。

数组遍历

所谓 "遍历" 是指将数组中的所有元素都访问一遍(通常需要搭配循环语句),访问是指对数组中的元素进行某种操作,比如,打印

数组的打印 (三种方式):

第一种方法 使用for 循环 遍历数组

第二种方法 使用 for-each 遍历数组

public static void main(String[] args) {
    int[] array = {1,2,3,4};

    // 通过for循环遍历数组
    for (int i = 0; i < array.length; i++) {
        System.out.print(array[i] + " ");
    }

    System.out.println();

    //通过 for-each 遍历数组
    for (int x: array) {
        System.out.print(x+" ");;
    }
}
//运行结果
1 2 3 4 
1 2 3 4 

for-each 是 for 循环的另外一种使用方式:

for-each循环基本语句:

for ( 变量类型 变量名 : 数组名 ) {

需要执行的循环语句;

}

其中变量类型与数组内元素类型相同的类型。

遍历这个数组的时候,把数组当中的元素赋值给 x (变量名)

for循环 和 for each 循环的区别?

  • for循环可以拿到数组下标;for each 拿不到数组下标 —— 更多会用到集合中
  • for循环是通过循环控制变量,访问数组中不同位置的元素从而进行遍历。
  • for-each循环是通过与数组内元素类型相同的变量进行遍历。直接得到数组内从下标为0的位置至最后一个位置的元素的元素值,便于数组内元素的查找。比如在数组内我只需要找到是否有某个元素,而不用返回元素对应的数组下标,这种情况下for-each循环是一个不错的选择。
  • for-each循环能够更方便的完成对数组的遍历,可以避免循环条件和更新语句写错

第三种方法,使用Arrays工具类中的toString方法

使用操作数组的工具类必须导包进入项目中 Arrays的包为 java.util.Arrays

import java.util.Arrays — 导入包;与C中的导入头文件相似,include

toString 是操作数组的工具类下的一个方法

什么是包?(先做了解)

例如做一碗油泼面,需要先和面,面,扯出面条,再烧水,下锅煮熟,放调料,泼油。
但是其中的"和面,排面,扯出面条"环节难度比较大,不是所有人都能很容易做好,于是超市就提供了一些直接已经扯好的面条,可以直接买回来下锅煮,从而降低了做油泼面的难度,也提高了制作效率。
程序开发也不是从零开始,而是要站在巨人的肩膀上。
像我们很多程序写的过程中不必把所有的细节都自己实现,已经有大量的标准库(JDK提供好的代码)和海量的第三方库(其他机构组织提供的代码)供我们直接使用。这些代码就放在一个一个的"包"之中。所谓的包就相当于卖面条的超市,只不过,超市的面条只有寥寥几种,而我们可以使用的"包",有成千上万。

2. 数组是引用类型

2.1 初始JVM的内存分布

内存是一段连续的存储空间,主要用来存储程序运行时数据的。比如:

1. 程序运行时代码需要加载到内存
2. 程序运行产生的中间数据要存放在内存
3. 程序中的常量也要保存
4. 有些数据可能需要长时间存储,而有些数据当方法运行结束后就要被销毁
如果对内存中存储的数据不加区分的随意存储,那对内存管理起来将会非常麻烦。比如:

因此JVM也对所使用的内存按照功能的不同进行了划分:

JVM实际上是由C/C++代码实现的一个软件而已,分为5个内存:

  1. java虚拟机栈:常说的栈 存放的是局部变量和引用
  2. : 存储对象   (堆里面存储的是对象,每个对象在堆里都有一个地址)
  3. 本地方法栈:存放一些编写JVM虚拟机的C/C++代码
  4. 方法区存放静态的变量
  5. 程序计数器:记录下一步的指令

更详细的解读:

  • 虚拟机栈(JVM Stack): 与方法调用相关的一些信息,每个方法在执行时,都会先创建一个栈帧,栈帧中包含有:局部变量表、操作数栈、动态链接、返回地址以及其他的一些信息,保存的都是与方法执行时相关的一些信息。比如:局部变量。当方法运行结束后,栈帧就被销毁了,即栈帧中保存的数据也被销毁了。
  • 本地方法栈(Native Method Stack): 本地方法栈与虚拟机栈的作用类似. 只不过保存的内容是Native方法的局部变量. 在有些版本的 JVM 实现中(例如HotSpot), 本地方法栈和虚拟机栈是一起的。
  • 堆(Heap): JVM所管理的最大内存区域,使用 new 创建的对象都是在堆上保存 (例如前面的 new int[]{1,2,3} ),堆是随着程序开始运行时而创建,随着程序的退出而销毁,堆中的数据只要还有在使用,就不会被销毁。
  • 方法区(Method Area): 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. 方法编译出的的字节码就是保存在这个区域。
  • 程序计数器 (PC Register):只是一个很小的空间, 保存下一条执行的指令的地址

局部变量和引用保存在栈上,new 出的对象保存在堆上。堆的空间非常大,栈的空间比较小。堆是整个JVM共享一个,而栈每个线程具有一份(一个Java程序中可能存在多个栈)。

现在我们只简单关心堆 和 虚拟机栈这两块空间,后序JVM中还会更详细介绍。

Native方法:

JVM是一个基于C++实现的程序。在Java程序执行过程中,本质上也需要调用C++提供的一些函数进行和操作系统底层进行一些交互。因此在Java开发中也会调用到一些C++实现的函数。
这里的Native方法就是指这些C++实现的,再由Java来调用的函数。native实现的方法,特点:快。

2.2 基本类型变量与引用类型变量的区别

基本数据类型创建的变量,称为基本变量,该变量空间中直接存放的是其所对应的值
引用数据类型创建的变量,一般称为对象的引用(也叫引用变量,简称引用,其空间中存储的是对象所在空间的地址

public static void func() {
int a = 10;
int b = 20;
int[] array = new int[]{1,2,3,4,5,6};
}

在上述代码中,a、b、arr,都是函数内部的变量,因此其空间都在main方法对应的栈帧中分配。
a、b是内置类型的变量,因此其空间中保存的就是给该变量初始化的值。
array是数组类型的引用变量,其内部保存的内容可以简单理解成是数组在堆空间中的首地址

图解:

  • 从上图可以看到,引用变量并不直接存储对象本身,可以简单理解成存储的是对象在堆中空间的起始地址。通过该地址,引用变量便可以去操作对象。有点类似C语言中的指针,但是Java中引用要比指针的操作更简单。
  • array是一个变量存放着地址,所以array被叫做引用变量,简称引用
  • 不管指针还是引用,里面存的都是地址;引用与C指针类似,但是不需要解引用。
  • 引用存储的是对象的地址,指向这个对象。描述为:array 这个引用 指向了 一个数组对象
  • int[ ] array2 = null;  这个引用 不指向 任何的对象

什么是引用?

引用相当于一个"别名",也可以理解成一个指针。
创建一个引用只是相当于创建了一个很小的变量,这个变量保存了一个整数,这个整数表示内存中的一个地址。

2.3 认识null

null 在 Java 中为 "空引用",表示不引用任何对象 —— 无效的引用。类似于 C 语言中的空指针,如果对 null 进行  操作就会引发异常。

下面情况也会报空指针异常:

  • 出现空指针异常就去找哪个引用是空的
  • null的作用类似于C语言中的NULL(空指针),都是表示一个无效的内存位置。因此不能对这个内存进行任何读写操作,一旦尝试读写,就会抛出NullPointerException(空指针异常)
  • Java中并没有约定null和0号地址的内存有任何关联。

打印结果是 null 字符串

3. 作为函数参数传递

3.1 参数传基本数据类型

public static void main(String[] args) {
    int num = 0;
    func(num);
    System.out.println("num = " + num);
}
public static void func(int x) {
    x = 10;
    System.out.println("x = " + x);
} 

// 执行结果
x = 10
num = 0

发现在func方法中修改形参 x 的值,不影响实参的 num 值。
 

3.2 参数传数组类型(引用数据类型,重点图解)

按引用传递,通过形参引用改变了实参引用指向对象的内容

  • 如上图所示,实参与形参 引用都指向了一个对象,在func方法内部修改数组的内容,方法外部的数组内容也发生改变。因为数组是引用类型,按照引用类型来进行传递,是可以修改其中存放的内容的。
  • 在java中引用变量里面就是存了一个值,只不过这个值是代表地址而已;引用变量是在栈区上的,而对象是在堆区上的,所以可以通过引用变量中的值找到堆里面对应的对象 —— 如果只针对数组,格局就变小了,还有其他变量。

在C语言中,例如数组 —— 开辟的局部变量都在栈中

int a[100] = {0} // 这是在栈中的

int a[100] = malloc(sizeof(int)*100); // 这是在堆中的

按引用传递,改变形参引用的指向,即改变了所指向的对象。

  • 上图打印的结果是实参引用的引用对象的值。调用func1方法传参虽然传过去的是地址,但是仅仅是一个值只是代表地址(形参引用能通过这个地址找到堆里面的对象),而形参重新new了一个数组对象,改变了自己的指向,即改变了所指向的对象。因此改变形参引用指向对象的内容,不影响实参引用指向的对象中内容。
  • 当一个对象没有引用指向的时候,就会被系统自动回收

总结:

所谓的 "引用" 本质上只是存了一个地址Java 将数组设定成引用类型,这样的话后续进行数组参数传参,其实只是将数组的地址传入到函数形参中。这样可以避免对整个数组的拷贝(数组可能比较长,那么拷贝开销就会很大),从而比较高效。

3.3 引用中常遇见的问题

1、一个引用 指向了 另一个引用 所指向的对象

[ 数组(以 [ 开头,配合其他的特殊字符,表述对应数据类型的数组,几个 [ 表述几维数组)

I 表示数组是 int[] 类型的。@是固定写法。 @后面的数字和字母就是地址。

2、这个引用不指向任何对象

int[] array = null;

3、一个引用不能同时指向多个对象

一个引用只能保存一个对象的地址,上图打印的是 new int[12] 对象的地址。

4、引用不一定是在栈上

一个变量在不在栈上,是该变量的性质决定的,例如 局部变量是在栈上的;实例成员变量,不一定在栈上。(后面会专门讲解)

Java的变量类型分为:

  1. 成员变量类中的变量(独立于方法之外的变量)
  2. 局部变量类方法中的变量。

而 java类的成员变量又分为:

  1. 静态变量(类变量)独立于方法之外的变量,用 static 修饰。
  2. 实例变量独立于方法之外的变量,不过没有 static 修饰。

在语法定义上的区别:

静态变量前要加static关键字,而实例变量前则不加。

————————————————

版权声明:本文为优快云博主「代码匪徒」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:成员变量、实例变量、局部变量、类变量(静态变量)详解-优快云博客

4. 数组作为方法的返回值

比如:在原来数组上,数组中的元素扩大2倍

直接修改原数组

这个代码固然可行,但是破坏了原有数组。有时候我们不希望破坏原数组,就需要在方法内部创建一个新的数组,并由方法返回出来。

返回一个新的数组

这样的话就不会破坏原有数组了。
另外由于数组是引用类型,返回的时候只是将这个数组的首地址返回给函数调用者,没有拷贝数组内容,从而比较高效。

比如:获取斐波那契数列的前N项

public class TestArray {
    public static int[] fib(int n){
        if(n <= 0){
            return null;
        } 
        int[] array = new int[n];
        array[0] = array[1] = 1;
        for(int i = 2; i < n; ++i){
            array[i] = array[i-1] + array[i-2];
        } 
        return array;
    }
    public static void main(String[] args) {
        int[] array = fib(10);
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + " ");
        }
    }
}

//运行结果
1 1 2 3 5 8 13 21 34 55 

5. 关于数组经典例题

Java 中提供了 java.util.Arrays 包,其中包含了一些操作数组的常用方法

5.1 数组转字符串

1、模拟实现数组转字符串

public static String myToString(int[] array) {
    if (array == null) return "null";//防止传过来的参数为空指针,需要进行判断一下
    // java中也有类似于C中assert断言,但是需要手动开启在IDEA中设置一下
    String str = "[";
    for (int i = 0; i < array.length; i++) {
        //借助 String + 其他数据类型  进行拼接字符串
        str += array[i];
        // 除了最后一个元素之外, 其他元素后面都要加上 ","
        if (i != array.length - 1) {
            str += ", ";
        }
    }
    str += "]";
    return str;
}

public static void main10(String[] args) {
    int[] array = {1, 2, 3, 4, 5};
    System.out.println(myToString(array));
}

//运行结果
[1, 2, 3, 4, 5]

2、使用toString方法

Java 中提供了 java.util.Arrays 包,其中包含了一些操作数组的常用方法

使用toString这个方法后续打印数组就更方便一些。

import java.util.Arrays

int[] arr = {1,2,3,4,5,6};
System.out.println(Arrays.toString(arr));

// 执行结果
[1, 2, 3, 4, 5, 6]

5.2 数组的拷贝

1、模拟实现数组的拷贝

import java.util.Arrays

public static int[] copyArray(int[] array) {
    if (array == null) return null;

    int[] copy = new int[array.length];
    for (int i = 0; i < array.length; i++) {
        copy[i] = array[i];
    }
    return copy;
}

public static void main(String[] args) {
    int[] array = {1, 2, 3, 4, 5, 6};
    System.out.println(array);

    int[] ret = copyArray(array);
    System.out.println(ret);
    System.out.println(Arrays.toString(ret));
}

//运行结果
[I@7f31245a
[I@6d6f6e28
[1, 2, 3, 4, 5, 6]

模拟实现new了一个新的对象,从而使形参引用的指向了新的对象。故改变拷贝后数组中的内容不会影响原数组中的内容。

2、用copyOf方法进行拷贝

import java.util.Arrays

//用copyOf方法进行拷贝
public static void main(String[] args) {
    int[] array = {1, 2, 3, 4, 5, 6};
    System.out.println(array);
    System.out.println(Arrays.toString(array));

    int[] ret = Arrays.copyOf(array, array.length * 2);
    System.out.println(ret);
    System.out.println(Arrays.toString(ret));
}
//运行结果
[I@7f31245a
[1, 2, 3, 4, 5, 6]
[I@6d6f6e28
[1, 2, 3, 4, 5, 6, 0, 0, 0, 0, 0, 0]

查看copyOf的源码可知,new了一个新的数组对象进行拷贝,数组作为返回值;

copyOf也可以进行扩容,即new一个新的对象,进行扩容操作,不能是小数的扩容。例如 1.5倍 只能是整数倍扩容。

copyOf底层其实是 调用了 arraycopy这个方法进行拷贝。

拷贝某个范围,使用cofyOfRanje方法

import java.util.Arrays

int[] array = {1, 2, 3, 4, 5, 6};
int[] ret = Arrays.copyOfRange(array,2,5);//左闭右开区间
System.out.println(Arrays.toString(ret));

//运行结果
[3, 4, 5]

java中见到from, to都是左闭右开区间

查看copyOfRange的源码,new了一个新的数组对象进行操作,数组作为返回值。

copyOfRanje底层其实是 调用了arraycopy这个方法进行拷贝和扩容的。

3、直接用System.arraycopy方法进行拷贝

import java.util.Arrays

public static void main(String[] args) {
    int[] array = {1, 2, 3, 4, 5, 6};
    int[] copy = new int[array.length];
    System.arraycopy(array,0,copy,0,array.length);
    System.out.println(Arrays.toString(copy));
}
//运行结果
[1, 2, 3, 4, 5, 6]

arraycopy方法的源代码解读:

4、通过数组名调用clone方法 — 克隆

import java.util.Arrays;

public static void main(String[] args) {
    int[] array = {1, 2, 3, 4, 5, 6};
    System.out.println(array);
    System.out.println(Arrays.toString(array));

    int[] copy = array.clone();
    System.out.println(copy);
    System.out.println(Arrays.toString(copy));
}

//运行结果
[I@7f31245a
[1, 2, 3, 4, 5, 6]
[I@6d6f6e28
[1, 2, 3, 4, 5, 6]

深拷贝和浅拷贝这里不做深入介绍,等到接口的时候会详细讲解。

// newArr和arr引用的是同一个数组
// 因此newArr修改空间中内容之后,arr也可以看到修改的结果
int[] arr = {1,2,3,4,5,6};
int[] newArr = arr;
newArr[0] = 10;
System.out.println("newArr: " + Arrays.toString(arr));

这种不属于拷贝,只是让 newArr引用 指向了 arr引用所指的对象。

Java中的数组有对应的类么,为什么数组可以直接调用clone()方法?

  1. Java中并不存在任何一个类对应数组,数组属于Java语言的一部分。
  2. 数据是特殊的对象,本身就实现了Cloneable。Object的clone方法的javadoc中有这么一句Note that all arrays are considered to implement the interface Cloneable;(所有数组都被认为是实现接口Cloneable)所以数组是可以直接使用clone方法的。
  3. 数组对象天生就有一个final的length属性。

数组类的本质

  1. JVM 动态生成的类
    Java 数组在运行时由 JVM 自动生成对应的类,而非通过显式定义的 Java 源码实现。这些类的命名遵循 JVM 规范(如 int[] 对应 [IString[] 对应 [Ljava.lang.String;),且隐式继承自 Object 类‌。

  2. 未暴露源码的实现细节
    数组类属于 JVM 内部实现的一部分,开发者无法直接查看或修改其源码。所有与数组相关的操作(如内存分配、索引访问)均由 JVM 底层指令(如 arraylength)直接处理‌。

5.3 查找数组中指定元素(顺序、二分查找)

给定一个数组,在给定一个元素,找出该元素的数组中的位置

顺序查找

调用函数传过去的参数也需要判断是否为null空指针,一般出现null是让程序抛出异常,因为还没有学遇见null抛出异常(C中的断言assert,遇见传参为null空指针会抛出异常),所以该业务先处理为传参过去遇见null返回-1的值。

public static int find(int[] arr, int key) {
    if (arr == null) return -1; //业务上的处理;一般出现null就让它抛出异常
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] == key) {
            return i;
        }
    }
    return -1;//表示没有找到
}

public static void main(String[] args) {
    int[] arr = {10, 6, 5, 2, 8};
    System.out.println(find(arr, 2));
}
//运行结果
3

二分查找

针对有序数组,可以使用更高效的二分查找。

啥叫有序数组?
有序分为 "升序" 和 "降序"
如 1 2 3 4 , 依次递增即为升序.
如 4 3 2 1 , 依次递减即为降序

思路:

以升序数组为例,二分查找的思路是先取中间位置的元素,然后使用待查找元素与数组中间元素进行比较

  • 如果相等,即找到了返回该元素在数组中的下标
  • 如果小于,以类似方式到数组左半侧查找
  • 如果大于,以类似方式到数组右半侧查找

left、right和mid都是数组下标

画图

代码实现:

public static int binarySearch(int[] arr, int key) {
    if (arr == null) return -1; //业务上的处理;一般出现null就让它抛出异常

    int left = 0;
    int right = arr.length - 1;

    while (left <= right) {
        int mid = (left + right) / 2;
        if (key < arr[mid]) {
            right = mid - 1;
        } else if (key > arr[mid]) {
            left = mid + 1;
        } else {
            return mid;
        }
    }
    //循环结束,说明没找到
    return -1;
}

public static void main(String[] args) {
    int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    System.out.println(binarySearch(array, 1));
    System.out.println(binarySearch(array, 6));
    System.out.println(binarySearch(array, 9));
}
//运行结果
0
5
8
  • 注意体会查找数组最后一个元素时,循环条件 left = fight 的情况
  • 我们在进行手动测试的时候,尽量找数组中第一个值、中间值和最后一个值,这样避免一些特殊的情况发生。
  • 代码的完成度不需要一步到位,需要我们在慢慢调试中完善。调试的时候可以画图进行,边调试边画图。

5.4 数组排序(冒泡排序)

给定一个数组,让数组升序 (降序) 排序。

思路:

假设排升序:

  1. 将数组中相邻元素从前往后依次进行比较,如果前一个元素比后一个元素大,则交换,一趟下来后最大元素就在数组的末尾
  2. 依次从上述过程,直到数组中所有的元素都排列好

第一次优化:每趟 j 走的次数都比上一次少1。

第二次优化:每一趟是否交换来判断上一趟是否已经有序,提前结束不用再排下一趟了。排序的过程中,不知道哪一趟数组就有序了。例如:第二趟排完就有序了,第三趟检查后没交换,剩下的就不需要比较了。

代码实现:

import java.util.Arrays;

public static void bubbleSort(int[] arr) {
    if (arr == null) {
        // 实际上数组为null,是要抛出异常
        System.out.println("该数组为null");
    }

    // 控制比较的趟数
    for (int i = 0; i < arr.length - 1; i++) {

        boolean flg = false;// 又一次优化
        // 控制比较的次数  //优化,每次比上一次少1次比较
        for (int j = 0; j < arr.length - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
                flg = true;
            }
        }
        // 判断如果比较一趟没有交换,就不用再继续比较
        if (!flg) {
            // 没有交换
            return;
        }
    }
}


public static void main(String[] args) {
    int[] array = {22, 99, 88, 66, 55};
    bubbleSort(array);
    System.out.println(Arrays.toString(array));
}
//运行结果
[22, 55, 66, 88, 99]

还有哪些排序方式:

  • 基于比较排序:插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序
  • 不需比较大小排序:基数排序、计数排序、桶排序

5.5 数组的逆序

给定一个数组,将里面的元素逆序排列

思路:

  1. 设定两个下标,分别指向第一个元素和最后一个元素,交换两个位置的元素。
  2. 然后让前一个下标自增,后一个下标自减,循环继续即可。

代码实现:

import java.util.Arrays;

public static void reverse(int[] arr) {
    if (arr == null) {
        System.out.println("该数组为null");
    }
    int i = 0;
    int j = arr.length - 1;
    while (i < j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
        i++;
        j--;
    }
}

public static void main17(String[] args) {
    int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    reverse(array);
    System.out.println(Arrays.toString(array));
}
//运行结果
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

5.6 数组数字(奇偶)排列

给定一个整型数组,将所有的偶数放在前半部分,将所有的奇数放在数组后半部分

例如:{1, 2, 3, 4} 调整后得到  {4, 2, 3, 1}

思路:

  1. 设定两个下标分别指向第一个元素和最后一个元素。
  2. 用前一个下标从左往右找到第一个奇数,用后一个下标从右往左找到第一个偶数,然后交换两个位置的元素。

代码实现:

import java.util.Arrays;

public static void func(int[] arr) {
    if (arr == null) return;
    int i = 0;
    int j = arr.length - 1;

    while (i < j) {
        // 从左往右找到第一个奇数
        while (i < j && arr[i] % 2 == 0) {
            i++;
        }
        // 从右到左找到第一个偶数
        while (i < j && arr[j] % 2 != 0) {
            j--;
        }
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

public static void main(String[] args) {
    int[] array = {1, 2, 3, 4, 5, 6};
    func(array);
    System.out.println(Arrays.toString(array));
}
//运行结果
[6, 2, 4, 3, 5, 1]

注意体会,循环找奇数和偶数时 i < j 的判断条件

6. 二维数组

6.1 基本概念

二维数组本质上也就是一维数组,只不过每个元素又是一个一维数组。

语法格式:数据类型[ ][ ] 数组名称 = new 数据类型 [行数][列数] { 初始化数据 };

int[][] array1 = {{1, 2, 3}, {4, 5, 6}};
int[][] array2 = new int[][] {{1,2,3},{1,2,3}};
int[][] array3 = new int[2][3];

//三种写法,前两种[ ] 内不能写数字

图示:

代码演示:

int[][] array1 = {{1, 2, 3}, {4, 5, 6}};
System.out.println(array1.length);
System.out.println(array1[0].length);

//运行结果
2
3
  • java中二维数组在初始化时可以省略列,但是不能省略行
  • 为什么可以省略,因为可以手动指定列

6.2 二维数组打印方式

二维数组的打印方式有三种

6.3 不规则二维数组

int[][] array = {{1,2},{4,5,6}};
for (int i = 0; i < array.length; i++) {
    for (int j = 0; j < array[i].length; j++) {
        System.out.print(array[i][j]+" ");
    }
    System.out.println();
}

//运行结果
1 2 
4 5 6
  • java中二维数组可以省略列,但是不能省略行 ——为什么可以省略,因为可以手动指定列,且在使用的时候必须进行赋值,否则打印的时候为null
  • C中二维数组可以省略行,但是不能省略列
  • null 在 Java 中为 "空引用",表示不引用任何对象。类似于 C 语言中的空指针,如果对 null 进行 . 操作就会引发异常

该二维数组的 列 没有赋值,而数组是引用类型的,里面的值默认是null

  正确写法:

为什么可以省略,因为可以手动指定列,且在使用的时候必须进行赋值,否则打印的时候为null

二维数组的用法和一维数组并没有明显差别,因此我们不再赘述。
同理,还存在 "三维数组","四维数组" 等更复杂的数组,只不过出现频率都很低。


代码能力不是一蹴而就的,需要大量的累积;代码的完成度也不需要一步到位,需要我们在慢慢调试中去完善。希望大家能共同进步,坚定踏实的走好今后的每一步。

好了,本篇博客到此就结束了。感谢你的阅读,希望对你有所帮助(哪怕只是一点点),期待大家的交流讨论以及点赞收藏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值