目录
数组的定义与使用
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个车位,从上图中可以看到:
- 数组中存放的元素类型相同
- 数组的空间是连在一起的
- 每个空间有自己的编号,起始位置的编号为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:表示数组长度
- 如果数组中存储元素类型为基本数据类型,默认值为基本数据类型对应的默认值,比如:
类型 默认值 byte 0 short 0 int 0 long 0 float 0.0f double 0.0 char \u0000 boolean false
\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
【注意事项】
- 数组是一段连续的内存空间,因此支持随机访问,即通过下标访问快速访问数组中任意位置的元素(使用[ ] 操作技能读取数据,也能修改数据)
- 下标从0开始,介于[0, N)之间不包含N,N为元素个数,
- 使用 数组对象.length 能够获取到数组的长度,. 这个操作为成员访问操作符。后面在面向对象中会经常用到。
- 下标访问操作不能超出有效范围 [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个内存:
- java虚拟机栈:常说的栈 存放的是局部变量和引用
- 堆: 存储对象 (堆里面存储的是对象,每个对象在堆里都有一个地址)
- 本地方法栈:存放一些编写JVM虚拟机的C/C++代码
- 方法区:存放静态的变量
- 程序计数器:记录下一步的指令
更详细的解读:
- 虚拟机栈(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、一个引用 指向了 另一个引用 所指向的对象
- array2 这个引用 指向了 array这个引用 所指向的对象。
- 引用 指向 引用:这句话错误的。引用只能指向对象。
- 上述代码可知,println() 方法中通过打印数组名直接打印出该数组的地址。根据上一篇博客中的方法签名可知【Java】方法的使用,传参 + 重载 + 递归 —— 有图有码有真相-优快云博客
[ 数组(以 [ 开头,配合其他的特殊字符,表述对应数据类型的数组,几个 [ 表述几维数组)
I 表示数组是 int[] 类型的。@是固定写法。 @后面的数字和字母就是地址。
2、这个引用不指向任何对象
int[] array = null;
3、一个引用不能同时指向多个对象
一个引用只能保存一个对象的地址,上图打印的是 new int[12] 对象的地址。
4、引用不一定是在栈上
一个变量在不在栈上,是该变量的性质决定的,例如 局部变量是在栈上的;实例成员变量,不一定在栈上。(后面会专门讲解)
Java的变量类型分为:
- 成员变量:类中的变量(独立于方法之外的变量)
- 局部变量:类方法中的变量。
而 java类的成员变量又分为:
- 静态变量(类变量): 独立于方法之外的变量,用 static 修饰。
- 实例变量: 独立于方法之外的变量,不过没有 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()方法?
- Java中并不存在任何一个类对应数组,数组属于Java语言的一部分。
- 数据是特殊的对象,本身就实现了Cloneable。Object的clone方法的javadoc中有这么一句Note that all arrays are considered to implement the interface Cloneable;(所有数组都被认为是实现接口Cloneable),所以数组是可以直接使用clone方法的。
- 数组对象天生就有一个final的length属性。
数组类的本质
-
JVM 动态生成的类
Java 数组在运行时由 JVM 自动生成对应的类,而非通过显式定义的 Java 源码实现。这些类的命名遵循 JVM 规范(如int[]
对应[I
,String[]
对应[Ljava.lang.String;
),且隐式继承自Object
类。 -
未暴露源码的实现细节
数组类属于 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 数组排序(冒泡排序)
给定一个数组,让数组升序 (降序) 排序。
思路:
假设排升序:
- 将数组中相邻元素从前往后依次进行比较,如果前一个元素比后一个元素大,则交换,一趟下来后最大元素就在数组的末尾
- 依次从上述过程,直到数组中所有的元素都排列好
第一次优化:每趟 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 数组的逆序
给定一个数组,将里面的元素逆序排列
思路:
- 设定两个下标,分别指向第一个元素和最后一个元素,交换两个位置的元素。
- 然后让前一个下标自增,后一个下标自减,循环继续即可。
代码实现:
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}
思路:
- 设定两个下标分别指向第一个元素和最后一个元素。
- 用前一个下标从左往右找到第一个奇数,用后一个下标从右往左找到第一个偶数,然后交换两个位置的元素。
代码实现:
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
二维数组的用法和一维数组并没有明显差别,因此我们不再赘述。
同理,还存在 "三维数组","四维数组" 等更复杂的数组,只不过出现频率都很低。
代码能力不是一蹴而就的,需要大量的累积;代码的完成度也不需要一步到位,需要我们在慢慢调试中去完善。希望大家能共同进步,坚定踏实的走好今后的每一步。
好了,本篇博客到此就结束了。感谢你的阅读,希望对你有所帮助(哪怕只是一点点),期待大家的交流讨论以及点赞收藏。