⚡『LeetCode-Offer』算法刷题必备,数组理论基础

3,050 阅读8分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

虽然在平常开发中,使用集合(容器)的频率比数组高得多,不过集合的底层也是通过数组来实现的。而且,尽管集合相比数组来说强大得多,但是其执行效率远不及数组。所以在下一章讲集合之前,非常有必要深入了解一下数组。全文脉络思维导图如下:

1. 一维数组详解

所谓数组,就是相同数据类型的元素按一定顺序排列而成的集合。先来看看一维数组的三种声明和赋值方式:

第一种:

int[] a = {1, 2, 3};

第二种:

int[] b = new int[] {1, 2, 3};

第三种:

int[] c = new int[3];
c[0] = 1;
c[1] = 2;
c[2] = 3;

以上这三种方式的效果都是一样的,创建了一个存储 1、2、3 这三个整数的数组。

可以使用下面两种形式声明数组 :

int[] a;int a[];

通常都会使用第一种风格, 因为它将类型 int[] ( 整型数组)与变量名分开了。

我们来反编译一下这三段代码的 .class 文件,你就会发现其实在底层它们的创建方式都是一样的,编译器自动的给我们加上了 new 关键字,甚至还把 c 的声明和赋值一体化了。

另外,需要注意的是:new int[3]; 这条语句会创建一个能够存储 3 个元素的数组,不过该数组的最后一个元素的下标是 2(因为下标从 0 开始计数,相信我,刷算法题的时候,这个鬼东西经常会让你脑子短路)。并且这条语句会自动的初始化所有元素,比如对于 int 数组来说就是全部初始化为 0,对于 boolean 数组来说就会全部初始化为 false, 对象数组就会初始化为 null 等。

从上面这些代码和分析中,我们也不难看出,数组创建之后是无法改变其存储空间大小的(存储能力),尽管它可以改变每一个数组元素。

我们通过 IDEA 的联想功能来看看数组能够调用什么东西:

可以发现,数组拥有 Object 类的所有方法,并且还会新增一个属性 length(注意是属性,而不是方法),用来表示这个数组的长度,我们可以这样调用:a.length

注意区别于 String 类的 length() 方法,数组拥有的是 length 属性,而非方法。

综上,数组不仅能够封装数据,还能调用属性和方法,那这和对象有啥区别?没错,这也就是为什么说数组的本质是对象了。回顾一下我们之前总结的 Java 中方法参数的使用情况(按值调用):

  • 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
  • 一个方法可以改变一个对象参数的状态。
  • 一个方法不能让对象参数引用一个新的对象。

而因为数组的本质是对象,因此,将数组作为参数传递给方法,这个数组是可以被改变的。

OK,接下来,以下面这段代码为例,我们来看看一维数组在内存中的存储方式:

int[] b = new int[] {1, 2, 3};

int 数组对象 b 存储在 栈中,而数组元素既然是 new 出来的,那当然是存储在堆中。只有当 JVM 执行 new int[] 时,才会在堆中开辟相应的内存区域。

2. 多维数组详解

我们再来看看多维数组,就以二维数组为例,同样的三种声明与赋值方式:

第一种:

double[][] a = { 
    {16, 3, 2, 13}, 
    {5, 10, 11, 8}, 
    {9, 6, 7, 12}, 
    {4, 15, 14, 1} 
};

第二种:

// 构造一个 4 行 4 列的二维数组
double[][] b = new double[4][4] { 
    {16, 3, 2, 13}, 
    {5, 10, 11, 8}, 
    {9, 6, 7, 12}, 
    {4, 15, 14, 1} 
};

第三种:

double[][] c = new double[4][4];
c[0][0] = 16; // 第一行第一列值为 16
c[0][1] = 3; // 第一行第二列值为 3
c[0][2] = 2;
c[0][3] = 13;
c[1][0] = 5; // 第二行第一列值为 5
c[1][1] = 10;
c[1][2] = 1;
c[1][3] = 8;
......

同样的,我们来反编译一下这三段代码的 .class 文件,底层它们的创建方式基本也都是一样的,不过有些细微的差别。编译器还是自动的给我们加上了 new 关键字,不过没有像一维数组那样把 c 的声明和赋值一体化了。

到目前为止,我们所看到的数组与其他程序设计语言中提供的数组没有多大区别。但实际存在着一些细微的差异, 而这正是 Java 的优势所在:Java 实际上没有多维数组,只有一维数组。多维数组被解释为数组的数组。请看下图:

由于可以单独地存取数组的某一行, 所以可以让两行交换。

int[] temp = b[1];
b[1] = b[2];
b[2] = temp;

3. for each 循环

Java 有一种功能很强的循环结构, 可以用来依次处理数组中的每个元素而不必为指定下标值而分心。 这种增强的 for 循环的语句格式为:

for(variable : collection){
    // todo
}

collection 这一集合表达式必须是一个数组或者是一个实现了 Iterable 接口的类对象,例如 ArrayList

下面我们来对比一下使用下标遍历数组和使用 for each 循环遍历数组这两种方式:

// 使用下标遍历数组
int[] a = new int[100];
for(int i = 0; i < 100; i++) {
    a[i] = i;
}

// 使用 for each 循环遍历数组
for(int element: a){
    System.out.println(element);
}

for each 循环语句的循环变量将会遍历数组中的每个元素, 而不需要使用下标值。

不过,需要注意的是,for each 循环语句不能自动处理多维数组的每一个元素,它是按照行, 也就是一维数组处理的。以二维数组为例,要想访问二维数组的所有元素, 需要使用两个嵌套的循环, 如下所示:

int[][] a = { 
  {16, 3, 2, 13},
  {5, 10, 11, 8}, 
  {9, 6, 7, 12}, 
  {4, 15, 14, 1} 
};

for(int[] row : a) { // 遍历每一行
  for(int value : row) { // 遍历每一列
  	System.out.println(value);
  }
}

4. 可变参数

JDK 1.5 之后,如果我们定义一个方法需要接受多个参数,并且多个参数类型一致,我们可以对其简化成如下格式:

修饰符 返回值类型 方法名 (参数类型... 形参名){  }

... 用在参数上,称之为可变参数,它表明这个方法可以接收任意数量的参数。其实这个写法完全等价与

修饰符 返回值类型 方法名 (参数类型[] 形参名){  }

虽然同样是代表数组,但是在调用这个带有可变参数的方法时,不用创建数组,直接将数组中的元素作为实际参数进行传递,这就是简单之处。当然,其实这种方式的底层实现也是将这些元素先封装到一个数组中,在进行传递,不过这些动作都在编译 .class 文件时就自动完成了。

代码演示:

public class ChangeArgs {
    
    //可变参数写法
    public static int getSum(int... arr) {
        int sum = 0;
        for (int a : arr) {
            sum += a;
        }
        return sum;
    }
    
    public static void main(String[] args) {
        int[] arr = { 1, 4, 62, 431, 2 };
        int sum = getSum(arr);
        System.out.println(sum);
        
        int sum2 = getSum(6, 7, 2, 12, 2121);
        System.out.println(sum2);
    }
}

需要注意的是:如果在方法书写时,这个方法拥有多个参数,并且参数中包含可变参数,可变参数一定要写在参数列表的末尾

5. Arrays 类

Java 中,提供了一个很有用的数组工具类:java.util.Arrays。它提供的主要操作有:

1)Arrays.toString - 将一维数组转成字符串类型(打印一维数组的所有元素)

2)Arrays.deepToString - 将二维数组转成字符串类型(打印二维数组的所有元素)

3)Arrays.copyOf - 数组拷贝。举个例子,将 a 数组中的元素全部拷贝给 c 数组:

int[] c = Arrays.copyOf(a, 2 * a.length());

第 2 个参数是新数组的长度。这个方法通常用来增加新数组的大小:如果数组元素是数值型,那么多余的元素将被赋值为 0 ; 如果数组元素是布尔型,则将赋值为 false 等。相反,如果长度小于原始数组的长度,则只拷贝最前面的数据元素。

4)Arrays.sort - 对数组中的元素进行排序

5)Arrays.equals - Arrays 类提供了重载后的 equals 方法,用来基于内容比较数组,数组相等的条件是元素个数和对应位置的元素都相等。

6. 总结

不可否认,在 Java 中,数组是一种效率最高的存储和随机访问对象引用序列的方式。数组就是一个简单的线性序列,在内存中采用连续空间分配的存储方式,这使得通过下标访问元素非常快速。但是代价就是一旦创建了数组, 就不能再改变它的大小(尽管可以改变每一个数组元素)。

如果经常需要在运行过程中扩展数组的大小, 可以使用集合 ArrayList 。它可以通过创建一个新实例,然后把旧实例中所有的引用移到新实例中,从而实现更多空间的自动分配。但是这种弹性需要开销,因此,ArrayList 的效率比数组低很多。当然,无论数组还是集合,如果越界,都会得到一个 RuntimeException 异常。

关于集合会写成一个系列,下篇文章就会陆续开更,内容没啥难度,不过要记的东西非常多,啃完集合后面还有个硬骨头多线程,这俩学完 Java 基础部分基本就没啥了。

参考资料

  • 《Java 核心技术 - 卷 1 基础知识 - 第 10 版》
  • 《Thinking In Java(Java 编程思想)- 第 4 版》
  • 《On Java 8》中文版(《Java 编程思想》- 第 5 版)
  • 清浅池塘 - Java 中的数组-Java那些事儿:juejin.cn/post/684490…

如果小伙伴们和我一样总是犯愁刷题没有顺序和体系,最终导致半途而废的话,可以看这里哦:LeetCode-Offer: LeetCode + 剑指 Offer = 💰,博主目前东南大学硕士在读,也在准备秋招,大伙儿可以来这里一起每天刷题打卡呀~

我的 公众号『 飞天小牛肉 』,专注分享计算机基础(数据结构 + 算法 + 计算机网络 + 数据库 + 操作系统 + Linux)、Java 技术栈等相关原创技术好文。关注公众号第一时间获取文章更新,后台回复 300 即可免费获取极客大学出品的 Java 面试 300 题,回复 Echo 免费领取 star 1k+ 社区项目的配套教程