笔记根据动力节点Java老杜动力节点Java零基础视频教程(中部)编写。
第四章 数组
4.1 数组概述
- 什么是数组?
- 在Java中,数组是一种用于存储多个相同数据类型元素的容器。
- 例如一个存储整数的数组:
int[] nums = {100, 200, 300};
- 例如一个存储字符串的数组:
String[] names = {“jack”,“lucy”,“lisi”};
- 数组是一种引用数据类型,隐式继承Object。因此数组也可以调用Object类中的方法。
- 数组对象存储在堆内存中。
- 数组的分类?
- 根据维数进行分类:一维数组,二维数组,三维数组,多维数组。
- 根据数组中存储的元素类型分类:基本类型数组,引用类型数组。
- 根据数组初始化方式不同分类:静态数组,动态数组。
- Java数组存储元素的特点?
- 数组长度一旦确定不可变。
- 数组中元素数据类型一致,每个元素占用空间大小相同。
- 数组中每个元素在空间存储上,内存地址是连续的。
- 每个元素有索引,首元素索引0,以1递增。
- 以首元素的内存地址作为数组对象在堆内存中的地址。
- 所有数组对象都有length属性用来获取数组元素个数。末尾元素下标:
length-1
- 数组优点?
- 根据下标查询某个元素的效率极高。数组中有100个元素和有100万个元素,查询效率相同。时间复杂度O(1)。也就是说在数组中根据下标查询某个元素时,不管数组的长短,耗费时间是固定不变的。
- 原因:知道首元素内存地址,元素在空间存储上内存地址又是连续的,每个元素占用空间大小相同,只要知道下标,就可以通过数学表达式计算出来要查找元素的内存地址。直接通过内存地址定位元素。
- 数组缺点?
- 随机增删元素的效率较低。因为随机增删元素时,为了保证数组中元素的内存地址连续,就需要涉及到后续元素的位移问题。时间复杂度O(n)。O(n)表示的是线性阶,随着问题规模n的不断增大,时间复杂度不断增大,算法的执行效率越低。(不过需要注意的是:对数组末尾元素的增删效率是不受影响的。)
- 无法存储大量数据,因为很难在内存上找到非常大的一块连续的内存。
4.2 一维数组
-
一维数组是线性结构。二维数组,三维数组,多维数组是非线性结构。
-
如何静态初始化一维数组?
- 第一种:
数据类型[] 变量名 = new 数据类型[]{元素1,元素2,元素3……};
int[] arr = {55,67,22}; 或者 int arr[] = {55,67,22}; - 第二种:
数据类型[] 变量名 = {元素1,元素2,元素3……};
int[] arr = new int[]{55,67,22}; - 提醒:如果大家在创建数组对象的时候,提前知道数组中应该具有存储哪些元素,建议使用静态初始化。
- 第一种:
-
如何访问数组中的元素?
- 通过下标来访问。
- 注意
ArrayIndexOutOfBoundsException
异常的发生。(数组下标越界异常)
-
如何遍历数组?
- 普通for循环遍历
- for-each遍历(优点是代码简洁,可读性强。缺点是没有下标。)
- 语法结构:
for(数据中元素的数据类型 变量名 : 数组名){}
- 注意:变量名 代表数组中的每个元素
- 语法结构:
- 练一练:获取10个学生成绩,然后把成绩保存在数组中,接着遍历数组获得学生成绩,最后计算总分和平均分。
-
一维数组的动态初始化
- 什么时候使用动态吃书画一维数组呢?
- 当创建数组时,不知道数组中具有哪些元素,可以使用动态初始化。
- 语法格式:
数据类型[] 变量名 = new 数据类型[长度];
- 动态初始化一维数组后,数组长度确定,数组中存储的每个元素将采用默认值。
- 什么时候使用动态吃书画一维数组呢?
-
当一个方法的参数是一个数组的时候,我们怎么传参数?
- 第一种方式:创建好数组对象,然后传进去。
- 第二种方式:直接传
// 第一种方式(静态初始化) int[] nums = {1,2,3,4}; display(nums); //第二种方式 display(new int[]{1,2,3,4}); // 动态初始化 display(new int[10]);
-
方法在调用时如何给方法传一个数组对象?
-
当一维数组中存储引用时的内存图?
-
如何获取数组中的最大值?
- 假设首元素是最大的,然后遍历数组中所有元素,只要有更大的,就将其作为最大值。
- 思考:找出最大值的下标怎么做?
public class ArrayTest06 { public static void main(String[] args) { int[] arr = {1,3,3,4,45,56,6,7,87,8,8898,1,2,1}; System.out.println(arr.length); int max = searchMax(arr); System.out.println("最大值是:" + max); int maxIndex = searchMaxIndex(arr); System.out.println("最大值的下标是:" + maxIndex); } /** * 找最大值的下标 * @param arr 数组 * @return 最大值的下标 */ public static int searchMaxIndex(int[] arr) { // 假设第一个元素是最大的。 int maxIndex = 0; // 遍历数组 for (int i = 0; i < arr.length; i++) { if(arr[i] > arr[maxIndex]) { maxIndex = i; } } return maxIndex; } /** * 从arr数组中找最大值。 * @param arr 数组 * @return 最大值 */ public static int searchMax(int[] arr) { // 假设第一个是最大的 int max = arr[0]; // 遍历数组 /*for (int num : arr) { if(num > max){ max = num; } }*/ for (int i = 0; i < arr.length; i++) { if(arr[i] > max) { max = arr[i]; } } return max; } }
-
如果知道值,如何通过值找它的下标?
public class ArrayTest07 { public static void main(String[] args) { int[] arr = {200,1,33,4,5,110,56,120,6,120,120,7,4,4,43,3,3,3}; int elt = 200; int index = findIndexByElt(arr, elt); System.out.println(index >= 0 ? elt + "在数组中的下标是:" + index : "您查找的数据不存在!"); } /** * 从arr数组中查找elt元素的下标。(第一次出现处的下标) * @param arr 数组 * @param elt 要查找的元素 * @return 元素下标 */ public static int findIndexByElt(int[] arr, int elt){ for (int i = 0; i < arr.length; i++) { if(arr[i] == elt) { return i; } } // 找不到就返回-1 return -1; } }
-
如何将数组中的所有元素反转?
- 第一种方式:创建一个新的数组。
- 第二种方式:首位交换。
/** * 数组的反转 * 第一种方式:创建一个新数组。 * 第二种方式:首尾交换 * * 假设数据有偶数个: * {1,2,3,4} * 第一次循环:{4,2,3,1} * 第二次循环:{4,3,2,1} * * 假设数据有奇数个: * {1,2,3,4,5} * 第一次循环:{5,2,3,4,1} * 第二次循环:{5,4,3,2,1} * * 无论数组中的数据量是奇数还是偶数,循环的次数都是:length / 2 */ public class ArrayTest08 { // 首尾交换的方式完成数组的反转。 public static void main(String[] args) { int[] arr = {1,2,3,4,5,6,7,8,9}; // 反转 reverse(arr); // 遍历 for (int i = 0; i < arr.length; i++) { System.out.println(arr[i]); } } /** * 这种方式完成的数组反转,不但效率高(循环次数少),而且还节省空间,因为不需要new新的数组对象。 * @param arr */ public static void reverse(int[] arr){ for (int i = 0; i < arr.length / 2; i++) { // 首尾交换 // 首 arr[i] // 尾 arr[arr.length - 1 - i] int temp = arr[i]; arr[i] = arr[arr.length - 1 - i]; arr[arr.length - 1 - i] = temp; } } // 用创建一个新数组的方式进行数组的反转。 /*public static void main(String[] args) { int[] arr = {1,2,3,4,5,6,7,8,9}; // 反转 reverse(arr); // 遍历 for (int i = 0; i < arr.length; i++) { System.out.println(arr[i]); } } public static void reverse(int[] arr){ // 第一种方式:创建新数组 // 动态初始化一维数组 int[] newArr = new int[arr.length]; // 遍历新数组 for (int i = 0; i < newArr.length; i++) { newArr[i] = arr[arr.length - 1 - i]; } // 遍历新数组 for (int i = 0; i < newArr.length;i++) { arr[i] = newArr[i]; } }*/ // 用创建一个新数组的方式进行数组的反转。 /*public static void main(String[] args) { int[] arr = {1,2,3,4,5,6,7,8,9}; // 反转 int[] newArr = reverse(arr); // 遍历 for (int i = 0; i < newArr.length; i++) { System.out.println(newArr[i]); } } public static int[] reverse(int[] arr){ // 第一种方式:创建新数组 // 动态初始化一维数组 int[] newArr = new int[arr.length]; // 遍历新数组 for (int i = 0; i < newArr.length; i++) { newArr[i] = arr[arr.length - 1 - i]; } return newArr; }*/ }
-
关于main方法的形参args?
- 作用:接收命令行参数
- JVM负责调用ArrayTest.main()方法。JVM负责给main方法准备一个
String[]
一维数组的对象 - java ArrayTest abc def xyz底层JVM是怎么做的?
命令行参数:“abc def xyz”
JVM会将以上字符串以“空格”进行拆分,生成一个新的数组对象。最后这个数组对象是:String[] args={"abc","def","xyz"};
- 在DOS命令窗口中怎么传?在IDEA中怎么传?
-
关于方法的可变长度参数?
- 语法格式:
数据类型...
- 在形参列表中,可变长参数只能有一个,并且只能在参数列表的末尾出现。
- 可变长度参数可以当做数组来看待。
- 语法格式:
-
数组长度一旦确定不可变。
-
那数组应该如何扩容?
- 只能创建一个更大的数组将原数组中的数据全部拷贝到新数组中
- 可以使用
System.arraycopy()
方法完成数组的拷贝。
-
数组扩容会影响程序的执行效率,因此尽可能预测数据量,创建一个接近数量的数组,减少扩容次数。
public class ArrayTest11 { public static void main(String[] args) { // 原数组 int[] src = {1,3,3,4,45,5,6,7,78,8,8,9,9}; // 新数组 int[] dest = new int[src.length * 2]; // 拷贝 //System.arraycopy(src, 0, dest, 0, src.length); System.arraycopy(src, 3, dest, 1, src.length - 3); for(int num : dest) { System.out.println(num); } } }
4.3 二维数组
- 二维数组是一个特殊的一维数组,特殊在:这个一维数组中每个元素是一个一维数组。
- 二维数组的静态初始化
int[][] arr = new int[][]{{},{},{}};
int[][] arr = {{},{},{}};
- 二维数组的动态初始化(等长)
int[][] arr = new int[3][4];
- 二维数组的动态初始化(不等长)
int[][] arr = new int[3][];
- 二维数组中元素的访问
- 第一个元素:arr[0][0]
- 最后一个元素:
arr[arr.length-1][arr[arr.length-1].length-1]
- 二维数组中元素的遍历
// 动态初始化一个二维数组:等长 int[][] arr = new int[3][4]; for (int i = 0; i < arr.length; i++) { // arr[i] 是一个一维数组。 // 循环遍历一维数组 for (int j = 0; j < arr[i].length; j++) { System.out.print(arr[i][j] + " "); } // 换行 System.out.println(); }
// 动态初始化一个二维数组:不等长 int[][] nums = new int[3][]; nums[0] = new int[]{1,3,3,4}; nums[1] = new int[]{2,3,4}; nums[2] = new int[]{1,1,1,1,1,1,1,1,1,1}; // 遍历 for (int i = 0; i < nums.length; i++) { for (int j = 0; j < nums[i].length; j++) { System.out.print(nums[i][j] + " "); } System.out.println(); }
4.4 IDEA的Debug调试
- 在可能出现问题的代码附近添加断点。一般是将断点添加在方法体的某一行代码上。
- 断点可以添加多个。点一次添加一个断点。再点一次断点则消失。
- 添加断点后,如果想让程序运行到断点处停下来,需要使用Debug模式运行程序。
- Debug窗口中的按钮
- 给断点添加条件
- Debug窗口中的隐藏按钮
4.5 JUnit单元测试(Junit5)
-
什么是单元测试,为什么要进行单元测试?
- 一个项目是巨大的,只有保证你写的每一块都是正确的,最后整个项目才能正常运行。这里所谓的每一块就是一个单元。
-
做单元测试需要引入JUnit框架,JUnit框架在JDK中没有,需要额外引入,也就是引入JUnit框架的class文件(jar包)
-
单元测试类(测试用例)怎么写?
- 单元测试类名:
XxxTest
- 单元测试类名:
-
单元测试方法怎么写?
- 单元测试方法需要使用
@Test
注解标注。 - 单元测试方法返回值类型必须是void
- 单元测试方法形参个数为0
- 建议单元测试方法名:testXxx
- 单元测试方法需要使用
-
什么是期望值,什么是实际值?
- 期望值就是在程序执行之前,你觉得正确的输出结果应该是多少
- 实际值就是程序在实际运行之后得到的结果
-
常见注解:
@BeforeAll
@AfterAll
主要用于在测试开始之前/之后执行必要的代码。被标注的方法需要是静态的。@BeforeEach
@AfterEach
主要用于在每个测试方法执行前/后执行必要的代码。
-
单元测试中使用Scanner失效怎么办?
- 选中导航栏的“Help”,然后选中“Edit Custom VM Options…”
- 接着在“IDEA64.exe.vmoptions”文件中添加内容“-Deditable.java.test.console=true”
- 最后在重启IDEA即可解决
/**
* 单元测试方法
*/
@Test
public void testSum() {
System.out.println("testSum");
// 实际值:程序运行之后的结果
int actual = Math.sum(10, 20);
// 期望值:你觉得这个结果应该是多少
int expected = 30;
// 断言(断言机制)
Assertions.assertEquals(expected, actual);
}
4.6 数据结构与算法
4.6.1 数据结构概述
-
数据结构是指用来存储和组织数据的一种方式,就像在生活中我们用文件柜、书架、衣柜等来整理我们的物品一样,数据结构也可以帮助我们整理和管理程序中的数据。
-
数据结构分为:数据的逻辑结构、数据的物理结构
- 逻辑结构是指数据元素之间的逻辑关系,它是从抽象的角度描述数据元素之间的关系,不涉及具体的存储方式或实现细节。逻辑结构主要关注问题的本质、特点和抽象模型,是数据结构的逻辑表示。
- 物理结构是指数据结构在计算机内存中实际存储和组织的方式。它是从具体的角度描述数据结构的实现方式和存储结构,包括数据元素在内存中的存储分布和访问方式等。物理结构主要关注问题的具体实现和操作。
- 因此,逻辑结构与物理结构的区别在于:逻辑结构是从抽象的角度描述数据元素之间的关系,物理结构是从具体的角度描述内存中数据元素的存储方式和组织形式。逻辑结构主要关注问题的本质和特点,物理结构主要关注问题的具体实现和操作。
-
逻辑结构的划分?
- 集合结构:数据结构中的元素之间除了在“同属一个集合”的关系外,别无其它关系;
- 线性结构:数据结构中的元素存在“一对一”的线性关系,例如冰糖葫芦;
- 树形结构:数据结构中的元素存在“一对多”的层次关系,例如公司组织架构;
- 图形结构或网状结构:数据结构中的元素存在“多对多”的任意关系,例如地图。
-
物理结构的划分?
- 顺序存储结构:用一组连续的存储空间单元来依次的存储数据元素,例如数组。
- 链式存储结构:用一组任意的存储单元来存储元素,通过保存地址找到相关联的元素,元素之间的逻辑关系用引用来表示,例如链表。
- 散列存储结构:根据节点key计算出该节点的存储地址。例如:java集合中的HashMap采用了散列存储结构,添加、查询速度都很快。
4.6.2 算法概述
-
什么是算法?
算法就是解决问题的方法和步骤,可以让计算机完成特定任务,并提高计算机系统的效率和性能。就像烹饪食品需要遵循一定的步骤和配方一样,例如,做牛排需要选择牛排肉、煎炸的方式、烹饪的时间等,按照一定的步骤最终会有一个好的成品。一个良好的算法可以提高程序的执行效率。 -
怎么评价一个算法好不好?
如何计算1+2+3+…+100的结果?- 算法1:通过循环,依次累加来实现。耗费时间
- 算法2:使用递归来实现。耗费内存
- 算法3:高斯算法。
(1 + 100)*50
。既节省时间,又节省空间。
同一问题可用不同的算法来解决,而一个算法的质量优劣将影响到算法乃至程序的效率。因此,我们学习算法目的在于选择合适算法和改进算法,一个算法的评价主要从时间复杂度和空间复杂度来考虑。
- 时间复杂度:评估执行程序所需的时间,可以估算出程序对处理器的使用程度。
- 空间复杂度:评估执行程序所需的存储空间,可以估算出程序对计算机内存的使用程度。
4.6.3 数据结构与算法的关系
- 程序的灵魂 = 数据结构 + 算法
- 数据结构可以提供算法的运行环境和基础,而算法又可以通过对数据结构的设计和操作,实现对数据的高效管理和处理。数据结构和算法是互相依存的,应该统一考虑,合理利用不同的数据结构和算法来解决实际问题,从而提高程序的执行效率。
4.6.4 时间复杂度
4.6.4.1 概述
- 什么叫做时间复杂度?
我们用T(n)来表示算法中基本操作(例如比较、赋值、运算等)的重复执行次数。n是程序需要处理的数据量(专业术语叫做问题规模n)。
若有某个趋势函数f(n)【f(n)代表了时间复杂度的趋势】,使得当n趋近于无穷大时,T(n)/f(n)
的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n))
,我们称O(f(n))为时间复杂度。我们也把它叫做大O表示法。 - 时间复杂度计算步骤:
- 计算基本操作的执行次数T(n)
在做算法分析时,一般默认考虑最坏的情况。因为最坏的情况下,基本操作执行的次数是最多的。对算法效率的评估会更加准确。 - 通过T(n)得到f(n)
求T(n)的数量级f(n),只需要将T(n)做两个操作:
(一)忽略常数项、低次幂项和最高次幂项的系数。
(二)例如,在T(n)=4n2+2n+2中,T(n)的数量级函f(n)=n2。
计算T(n)的数量级f(n),我们只要保证T(n)中的最高次幂正确即可,可以忽略所有常数项、低次幂项和最高次幂的系数。这样能够简化算法分析,将注意力集中在最重要的一点上:增长率。
- 计算基本操作的执行次数T(n)
- 用大O表示时间复杂度
得到f(n)的结果是n2,所以时间复杂度是:O(n2)。
切记,时间频度不相同,时间复杂度有可能相同,如T(n)=n2+3n+4与T(n)=4n2+2n+1它们的时间频度不同,但时间复杂度相同,都为O(n2)。
4.6.4.2 常见的时间复杂度
- 常数阶
O(1)
无论代码执行了多少行,只要没有循环等复杂结构,那这个代码的时间复杂度就都是O(1)
在上述代码中,没有循环等复杂结构,它消耗的时间并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度int num1 = 3, num2 = 5; int temp = num1; num1 = num2; num2 = temp; System.out.println("num1:" + num1 + " num2:" + num2);
- 对数阶
O(log2n)
O(log2n)指的就是:在循环中,每趟循环执行完毕后,循环变量都放大两倍。
int n = 1024;
for(int i = 1; i < n; i *= 2) {
System.out.println(“hello powernode”);
}
推算过程:假设该循环的执行次数为x次(也就是i的取值为2x),就满足了循环的结束条件,即满足了2x等于n,通过数学公式转换后,即得到了x = log2n,也就是说最多循环log2n次以后,这个代码就结束了,因此这个代码的时间复杂度为:O(log2n) 。
同理,如果每趟循环执行完毕后,循环变量都放大3倍,那么时间复杂度就为:O(log3n) 。 - 线性阶
O(n)
int n = 100;
for(int i = 0; i < n; i++) {
System.out.println(“hello powernode”);
}
在上述代码中,for循环会执行n趟,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度 。 - 线性对数阶
O(nlog2n)
线性对数阶O(nlog2n) 其实非常容易理解,将时间复杂度为O(log2n)的代码循环n遍的话,那么它的时间复杂度就是n*O(log2n),也就是了O(nlog2n)。int n = 100; for(int i = 1; i <= n; i++) { for(int j = 1; j <= n; j *= 2) { System.out.println("hello powernode"); } }
- 平方阶
O(n2)
外层i的循环执行一次,内层j的循环就要执行n次。因为外层执行n次,总的就需要执行n*n次,也就是需要执行n2次。因此这个代码时间复杂度为:O(n2)。int n = 100; for(int i = 1; i <= n; i++) { for(int j = 1; j <= n; j++) { System.out.println("hello powernode"); } }
平方阶的另外一个例子:
当i=1的时候,内侧循环执行n次,当i=2的时候,内侧循环执行(n-1)次,…一直这样子下去就可以构造出一个等差数列:n+(n-1)+(n-2)+…+2+1 ≈ (n2)/2。根据大O表示法,去掉最高次幂的系数,就可以得到时间复杂度为:O(n2)。int n = 100; for(int i = 1; i <= n; i++) { for(int j = i; j <= n; j++) { System.out.println("hello powernode"); } }
同理,立方阶 O(n3),参考上面的O(n2)去理解,也就是需要用到3层循环 - 指数阶
O(2n)
当n为10的时候,需要执行210次 - 阶乘阶
O(n!)
当n为10的时候,需要执行10*9*8*...*2*1
次
4.6.4.3 常见的时间复杂度耗时比较
- 算法的时间复杂度是衡量一个算法好坏的重要指标。一般情况下,随着规模n的增大,T(n)的增长较慢的算法为最优算法
- 其中x轴代表n值,y轴代表T(n)值。T(n)值随着n的值的变化而变化,其中可以看出O(n!)和O(2n)随着n值的增大,它们的T(n)值上升幅度非常大,而O(logn)、O(n)、O(nlogn)、O(n2)随着n值的增大,T(n)值上升幅度相对较小。
- 常用的时间复杂度按照耗费的时间从小到大依次是:
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!)
4.7 数组的排序算法
4.7.1 冒泡排序
- 冒泡排序的核心思想?
相邻两个元素做比较大小,如果前一个元素大于后一个元素,则交换位置 - 升序排序代码如何实现?
- 冒泡排序算法优化?
因为冒泡排序存在提前排序成功的可能,因此我们需要对以上冒泡排序算法进行优化,此处的优化思路使用了“假设法”来实现
public class ArrayTest14 {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
// 调用冒泡排序的方法进行冒泡排序
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
/*private static void bubbleSort(int[] arr) {
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]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}*/
/**
* 冒泡排序算法的优化。
* @param arr
*/
private static void bubbleSort(int[] arr) {
for (int i = arr.length - 1; i >= 0; i--) {
// 默认是排好序的。
boolean flag = true;
for (int j = 0; j < i; j++) {
System.out.println("=======================");
// 两个两个进行比较
if(arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
flag = false;
}
}
if(flag){
break;
}
}
}
}
4.7.2 选择排序算法
- 选择排序的核心思想?
- 核心原理:找出参与比较的这些数据中最小的,然后拿着这个最小的数据和参与比较的这堆数据中最左边的元素交换位置。
- 优点:交换的次数比冒泡排序的少。执行效率比冒泡排序高。(冒泡排序中的元素每一次比完之后就交换,这个交换有时是没必要的。)
- 升序排序代码如何实现?
原理:- (1)参与比较的数据:3, 2, 7, 6, 1
- 第一次循环之后的结果:1, 2, 7, 6, 3
- (2)参与比较的数据:x, 2, 7, 6, 3
- 第二次循环之后的结果:1, 2, 7, 6, 3
- (3)参与比较的数据:x, x, 7, 6, 3
- 第三次循环之后的结果:1, 2, 3, 6, 7
- (4)参与比较的数据:x, x, x, 6, 7
- 第四次循环之后的结果:1, 2, 3, 6, 7
- (1)参与比较的数据:3, 2, 7, 6, 1
public class ArrayTest {
public static void main(String[] args) {
int[] arr = {3, 2, 7, 6, 1, 100, 200, 80, 870};
selectSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void selectSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
// 假定参与比较的这些数据中最左边的是最小的。
int min = i;
for(int j = i+1; j < arr.length; j++){
if (arr[j] < arr[min]) {
min = j;
}
}
if(min != i){ // 说明有更小的值
// 交换位置(拿着当下的最小值和参与比较的这些数据中最前面的值交换位置)
int temp = arr[min];
arr[min] = arr[i];
arr[i] = temp;
}
}
}
}
4.8 数组的查找算法
4.8.1 线性查找
- 线性查找是一种最简单粗暴的查找法了,采用逐一比对的方式进行对数组的遍历,如果发现了匹配值,返回数组下标即可。
- 线性查找,优点是查找数组无需有序;其缺点是查找的次数多,效率低下。
public class ArrayTest {
public static void main(String[] args) {
int[] arr = {102,3,4,54,5,6,67,7,78,8,8,87,67,6};
// 找出以上数组中67元素的下标(67元素第一次出现处的下标)
int num = 670;
int index = search(arr, num);
System.out.println(index >= 0 ? num + "第一次出现处的索引是:" + index : "对不起,没有这个数据");
}
private static int search(int[] arr, int num) {
for (int i = 0; i < arr.length; i++) {
if(num == arr[i]){
return i;
}
}
return -1;
}
}
4.8.2 二分法查找
- 二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好;缺点是要求待查数组必须排序,且执行插入和删除操作困难。因此,折半查找方法适用于不经常变动而查找频繁的数组。
- 查找思路:
- 假设查找的数组为升序排序,则首先定义两个变量,分别用于保存查找元素(value)所在范围的最小索引值(min)和最大索引值(max)。
- 然后开启二分查找,每次查找前都定义一个mid变量,并设置该变量的初始值为:(max + min)/2。在查找的过程中,发生以下三种情况,则做对应的处理。
- 如果arr[mid]大于value,则证明查找的元素在mid的左侧,那么更新max的值为:mid-1
- 如果arr[mid]小于value,则证明查找的元素在mid的右侧,那么更新min的值为:mid+1
- 如果arr[mid]等于value,则证明查找元素的索引值就是mid,返回mid的值即可!
- 在以上的操作中,我们不停的更改min和max的值,如果发生min大于max的情况,则证明查找的元素不存在,那么返回-1(表示找不到)即可!
public class ArrayTest {
public static void main(String[] args) {
int[] arr = {1,20,50,77,80,99,101,256,666,888};
int num = 101;
int index = binarySearch(arr, num);
System.out.println(index >= 0 ? num + "的下标是:" + index : "找不到");
}
private static int binarySearch(int[] arr, int num) {
int begin = 0;
int end = arr.length - 1;
while(begin <= end){
int mid = (begin + end) / 2;
if(arr[mid] == num) {
return mid;
}else if(num > arr[mid]){
begin = mid + 1;
}else{
end = mid - 1;
}
}
return -1;
}
}
4.9 Arrays工具类
Arrays.toString()
方法:将数组转换成字符串@Test public void testToString(){ int[] arr = {1,2,3,34,54}; System.out.println(arr); // [I@3d3fcdb0 System.out.println(Arrays.toString(arr)); // [1, 2, 3, 34, 54] String[] names = {"zhangsan", "lisi", "wangwu"}; System.out.println(names); // [Ljava.lang.String;@641147d0 System.out.println(Arrays.toString(names)); //[zhangsan, lisi, wangwu] }
Arrays.deepToString()
方法:可以将多维数组转换成字符串@Test public void testDeepToString(){ // 适合于二维数组以及多维数组的。 int[][] arr = { {12,2,3,3}, {4,45,5,5}, {1,1,1,1,1,1} }; System.out.println(Arrays.toString(arr)); // [[I@641147d0, [I@6e38921c, [I@64d7f7e0] System.out.println(Arrays.deepToString(arr)); // [[12, 2, 3, 3], [4, 45, 5, 5], [1, 1, 1, 1, 1, 1]] }
Arrays.equals(int[] arr1, int[] arr2)
方法:判断两个数组是否相等@Test public void testEquals(){ int[] arr1 = {1,2,3}; int[] arr2 = {2,1,3}; System.out.println(Arrays.equals(arr1, arr2)); String[] names1 = new String[]{"abc", "def", "xyz"}; String[] names2 = new String[]{"abc", "def", "xyz"}; System.out.println(Arrays.equals(names1, names2)); }
Arrays.equals(Object[] arr1, Object[] arr2)
方法Arrays.deepEquals(Object[] arr1, Object[] arr2)
方法:判断两个多维数组是否相等Arrays.sort(int[] arr)
方法:基于快速排序算法,适合小型数据量排序。根据字典的顺序排序@Test public void testSort(){ int[] arr = {1,3,45,5,6,7,87,8}; Arrays.sort(arr); System.out.println(Arrays.toString(arr)); String[] strs = {"a", "ac", "ab", "b"}; // 应该是根据字典的顺序排序的。 Arrays.sort(strs); System.out.println(Arrays.toString(strs)); }
Arrays.sort(String[] arr)
方法Arrays.parallelSort(int[] arr)
方法:基于分治的归并排序算法,支持多核CPU排序,适合大数据量排序。/** * 启用多核CPU并行排序。 * 首先你的电脑是支持多核的。 * 注意:数据量太小的话,不要调用这个方法,因为启动多核也是需要耗费资源的。 * Java8引入的方法。 * 数据量较大的时候,建议使用这个方法效率比较高。 * * 通过源码分析:如果超过4096个长度,则会启用多核。 * 4096以内就自动调用sort方法就行了。 */ @Test public void testParallelSort(){ int[] arr = new int[100000000]; Random random = new Random(); for (int i = 0; i < arr.length; i++) { int num = random.nextInt(100000000); arr[i] = num; } // 获取系统当前时间的毫秒数(1970-1-1 0:0:0 000到当前系统时间的总毫秒数 1秒=1000毫秒) long begin = System.currentTimeMillis(); // 排序 Arrays.parallelSort(arr); // 获取系统当前时间的毫秒数 long end = System.currentTimeMillis(); // 耗时 System.out.println(end - begin); }
int binarySearch(int[] arr, int elt)
方法:二分法查找@Test public void testBinarySearch(){ int[] arr = {1,2,3,4,5,6,7}; System.out.println(Arrays.binarySearch(arr, 5)); }
Arrays.fill(int[] arr, int data)
方法:填充数组Arrays.fill(int[] a, int fromIndex, int toIndex, int val)
方法@Test public void testFill(){ int[] arr = new int[5]; // 5个0 Arrays.fill(arr, 10); System.out.println(Arrays.toString(arr)); // 不包含toIndex Arrays.fill(arr, 1, 3, 100); System.out.println(Arrays.toString(arr)); }
int[] Arrays.copyOf(int[] original, int newLength)
方法:数组拷贝int[] Arrays.copyOfRange(int[] original, int from, int to)
@Test public void testCopyOf(){ // 数组拷贝 int[] arr = {1,2,3,4,5,6,7,8,9}; int[] newArr = Arrays.copyOf(arr, 3); System.out.println(Arrays.toString(newArr)); // to不包含 int[] newArr2 = Arrays.copyOfRange(arr, 2, 4); System.out.println(Arrays.toString(newArr2)); }
Arrays.asList(T... data)
方法:将一组数据转换成List集合。@Test public void testAsList(){ // 将一串数字转换成List集合。 List list = Arrays.asList(1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } }
- 每章一句:“如果没有天赋,那就一直重复。”
- 恭喜你已阅读完第四章!点个赞证明你已经挑战成功,进入第五章关卡《异常》吧【更新中……】!