Java数组以及数组中涉及到的常见算法
编辑时间:2021/02/22
读完本节:大概花费40分钟,共4421词
1.数组元素的赋值(杨辉三角、回型数等)
-
杨辉三角
见“20210219Java数组”一文末尾。
-
回型数
实现如下效果:
方法一:逐层“口”字填充
使用while(end < start)控制进新层的循环
import java.util.Scanner; public class ArrayTestExer { public static void main(String[] args){ //回型数 //输入一个数得到如下输出 /* * 输入 * 1 * 输出 * 1 * * 输入 * 4 * 输出 * 1 2 3 4 * 12 13 14 5 * 11 16 15 6 * 10 9 8 7 * * */ //1. 获取输入数字 System.out.print("输入一个整数:"); Scanner scanner = new Scanner(System.in); int length = scanner.nextInt(); //2. 创建相应的二维数组 int[][] rectangle = new int[length][length]; int value = 1;//存于数值当中的值 //3. 给二维数组赋值 //每一层的长度在不停减小 int start = 0;//起点 int end = length - 1;//终点,数组索引范围是[0~length-1] while(end > start){ /* * 当end大于start时, * 循环继续,若用户给出的是偶数长度, * 则end = start还要在进行一次循环 */ //从左往右 for(int i = start;i <= end;i++){ //由于位值是某一行,所以行坐标为start //若length = 7,则为rectangle[0][0~6]赋值 rectangle[start][i] = value; //每赋完一个value,value自增1,为下一次赋值做准备 value++; } //从上往下 for(int i = start + 1;i <= end;i++){ //若length = 7,则为rectangle[1~6][6]赋值 rectangle[i][end] = value; //每赋完一个value,value自增1,为下一次赋值做准备 value++; } //从左往右 for(int i = end - 1;i > start - 1;i--){//往回倒 //若length = 7,则为rectangle[6][0~5]赋值 rectangle[end][i] = value; //每赋完一个value,value自增1,为下一次赋值做准备 value++; } //从下往上 for(int i = end - 1;i > start;i--){ //若length = 7,则为rectangle[1~5][0]赋值 rectangle[i][start] = value; //每赋完一个value,value自增1,为下一次赋值做准备 value++; } //进新层 start++; end--; } //5. 遍历数组 for(int i = 0;i < rectangle.length;i++){ for(int j = 0;j < rectangle[i].length;j++){ System.out.print(rectangle[i][j] + "\t"); } System.out.println(); } } }
方法二:螺旋填充
import java.util.Scanner; public class ArrayTestExer { public static void main(String[] args){ //1. 获取输入数字 System.out.print("输入一个整数:"); Scanner scanner = new Scanner(System.in); int length = scanner.nextInt(); //2. 创建相应的二维数组 int[][] rectangle = new int[length][length]; int value = 1;//存于数值当中的值 //3. 给数组元素赋值,方法二 //x轴最大下标,Y轴最大下标 int maxX = length-1, maxY = length-1; //X轴最小下标,Y轴最小下标 int minX = 0, minY = 0; while(minX <= maxX) { for(int x = minX;x <= maxX;x++) { rectangle[minY][x] = value++; } minY++;//最低行经过上述for已经被赋值,所以最低的行y自增1 for(int y = minY;y <= maxY;y++) { rectangle[y][maxX] = value++; } maxX--;//最高列经过上述for已经被赋值,所以最高的列x自减1 for(int x = maxX;x>= minX;x--) { rectangle[maxY][x] = value++; } maxY--;//最高行经过上述for已经被赋值,所以最高的行y自减1 for(int y = maxY;y>=minY;y--) { rectangle[y][minX] = value++; } minX++;//最低列经过上述for已经被赋值,所以最低的列x自增1 //通过执行完4个for完成了最外圈的赋值,并且通过while来判断是否已经到达最内层 //同时while中的判断语句也可以写成行的判断形式minY <= maxY } //5. 遍历数组 for(int i = 0;i < rectangle.length;i++){ for(int j = 0;j < rectangle[i].length;j++){ System.out.print(rectangle[i][j] + "\t"); } System.out.println(); } } }
-
练习:创建一个长度为6的int型数组,要求数组元素的值都在1~30之间,且都是随机值。同时要求元素的值各不相同。
public class ArrayTestExer { public static void main(String[] args){ //1. 创建一个长度为6int型数组 //动态声明 int[] arr = new int[6];//验证不重复可以改成30 for(int i = 0;i < arr.length;i++){ System.out.print(arr[i] + "\t"); } System.out.println(); //2. 要求元素的值应当是1~30之间的随机数,且元素值不重复 //元素赋值且不重复判定 for(int i = 0;i < arr.length;i++){ while(true){ //范围[a~b]的随机数(int)(Math.random() * (b - a + 1) + a); int random = (int)(Math.random() * (30 - 1 + 1) + 1); boolean isSame = false; //遍历当前已经赋过值的部分 for(int j = 0;j <= i;j++){ if(random == arr[j]){ //若有随机数等于其中元素,isSame标记为true //同时,无需再继续后续的j遍历 isSame = true; break; } } //当且仅当isSame是false时才会将值赋给arr[i],而且跳出while循环 if(!isSame){ arr[i] = random; break; } } } //打印输出数组 for(int i = 0;i < arr.length;i++){ System.out.print(arr[i] + "\t"); } } }
2.求数值型数组中元素的最大值、最小值、平均数、总和等
-
定义一个int的一维数组,包含10个元素,分别赋一些随机整数(所有随机数都是两位数),然后求出所有元素的最大值、最小值、和、平均值,并输出。
public class ArrayTestExer2 { public static void main(String[] args){ //1. 定义一个长度为10的int型一维数组 //动态初始化 int[] arr = new int[10]; //2. 赋上随机值[10~99] for(int i = 0;i < arr.length;i++){ int random = (int)(Math.random() * (99 - 10 + 1) + 10); arr[i] = random; } //声明变量 int maximum = arr[0]; int minimum = arr[0]; int sum = 0; double average = 0; for(int i = 0;i < arr.length;i++){ //maximum if(maximum < arr[i]){ maximum = arr[i]; } //minimum if(minimum > arr[i]){ minimum = arr[i]; } //sum sum += arr[i]; //average average = sum /10.0; } //打印 for(int i = 0;i < arr.length;i++){ System.out.print(arr[i] + "\t"); } System.out.println(); System.out.println("最大值是:" + maximum); System.out.println("最小值是:" + minimum); System.out.println("和是:" + sum); System.out.println("均值是:" + average); } }
3.数组的复制、反转、查找(线性查找、二分法查找)
-
数组的复制
//数组的复制 public class ArrayCopy{ public static void main(String[] args){ String[] arr = new String[]{"AA","BB","CC","DD","EE"}; String[] arr1 = new String[arr.length]; for(int i = 0;i < arr1.length;i++){ arr1[i] = arr[i]; } } }
练习:使用简单数组
- 创建一个名为ArrayTest的类,在main()方法中声明array1和array2两个变量,他们是int类型的数组
- 静态赋值array1{2,3,5,7,11,13,17,19}
- 打印array1
- 赋值array2=array1,修改array偶索引元素,使其等于索引值(如array[0] = 0; array[2] = 2;)
- 打印array1
/* array1和array2是什么关系? */ public class ArrayTest{ public static void main(String args){ int[] array1 = new int[]{2,3,5,7,11,13,17,19}; int[] array2 = new int[8]; //打印array1 for(int i = 0;i < array1.length;i++){ System.out.print(array1[i] + "\t"); } System.out.println(); //不能称做数组的复制 array2 = array1; //修改array2 for(int i = 0;i < array1.length;i++){ if(i % 2 == 0) { array2[i] = i; } } //打印array1 for(int i = 0;i < array1.length;i++){ System.out.print(array1[i] + "\t"); } } }
容易发现对array2元素的修改在array1上同样发生了,这种情况发生的原因是在堆空间中并没有对array2开辟新的空间,而是将array1在堆空间中开辟出来的首地址赋值给了array2,因此相当于array2的修改通过指向array1的元素,使array1的元素发生了改变。
-
数组的反转
public class ArrayReverse{ public static void main(String[] args){ //数组的反转 String[] reverse = new String[]{"AA","BB","CC","DD","EE"}; // //方法一: // for(int i = 0;i < reverse.length / 2;i++){ // //i < 长度的一半就行了,否则走到后一半又交换回来了,奇数长度除不尽中间一个元素刚好不用换 // String temp = reverse[i]; // reverse[i] = reverse[reverse.length - i - 1]; // reverse[reverse.length - i - 1] = temp; // } //方法二: for(int i = 0,j = reverse.length - 1;i < j;i++,j--){ //i从0开始,j从最高位开始,当i还在j前面时,交换。随后i自增,j自减 String temp = reverse[i]; reverse[i] = reverse[j]; reverse[j] = temp; } //遍历 for(int i = 0;i < reverse.length;i++){ System.out.print(reverse[i] + "\t"); } } }
-
数组的查找
数组的线性查找与二分法查找(折半查找)
public class ArraySearch { public static void main(String[] args){ String[] str = new String[]{"AA","BB","CC","DD","EE"}; //线性查找 String dest = "BB"; boolean isFind1 = false; for(int i = 0;i < str.length;i++){ if(dest.equals(str[i])){ System.out.println("找到了,元素位置是:" + i); isFind1 = true; break; } } if(!isFind1){ System.out.println("没找到"); } //二分法查找(折半查找) //折半查找的前提是数组必须有序 int[] arr = new int[]{-12,-10,-8,-2,0,5,9,10,15,20}; int head = 0;//初始索引 int end = arr.length - 1;//结束索引 int dest1 = 10; boolean isFind2 = false; while(head <= end){ int middle = (head + end) / 2;//中轴元素 if(dest1 == arr[middle]){ System.out.println("找到了,元素位置是:" + middle); isFind2 = true; break; }else if(dest1 < arr[middle]){ //查找middle的左半部分 end = middle - 1; }else{//dest1 > arr[middle] //查找middle的右半部分 head = middle + 1; } } if(!isFind2){ System.out.println("没找到"); } } }
折半查找过程图解:
4.数组元素的排序算法
-
排序:假设有n个记录的序列{R1,R2,…,Rn},其相应的关键字序列为{K1,K2,…,Kn}。将这些记录重新排序为{Ri1,Ri2,…Rin},使其相应的关键字序列值满足条件Ki1 <= Ki2 <= … <=Kin,这样的一种操作称为排序。通常来说排序的目的是快速查找。
-
衡量排序算法的优劣:
- 时间复杂度:分析关键字的比较次数和记录的移动次数
- 空间复杂度:分析排序算法中需要多少辅助内存
- 稳定性:若两个记录A和B的关键字值相等,但排序后的A、B的先后次序保持不变,则称这种排序算法是稳定的。
-
排序算法的分类:内部排序和外部排序
-
内部排序:整个排序过程不需要借助于外部存储器,所有排序操作都在内存中完成。
-
外部排序:参与排序的数据非常多,数据量非常大,计算机无法把整个排序过程放在内存中完成,必须借助外部存储器。外部排序最常见的是多路并归排序。可以认位外部排序是由多次内部排序组成的。
-
常见的内部排序算法:
选择排序:直接选择排序、堆排序
交换排序:冒泡排序、快速排序
插入排序:直接插入排序、折半插入排序、希尔排序
归并排序
桶式排序
基数排序
-
-
算法的5大特征:输入(input)、输出(output)、有穷性(finiteness)、确定性(definiteness)、可行性(effectiveness)
- 输入:由0个或多个输入数据,这些输入必须有清楚的描述和定义
- 输出:至少有1个或多个输出结果,不可以没有输出结果
- 有穷性:算法在有限的步骤之后会自动结束而不会无限循环,并且每一个步骤可以在可接受的时间内完成
- 确定性:算法中的每一步都有明确的含义,不会出现二义性。满足确定性的算法也称为确定性算法,相对的不确定算法有并行算法、概率算法等。
- 可行性:算法的每一步都是清楚且可行的,能让用户用纸笔计算而求出答案
-
冒泡排序算法:
-
冒泡算法的原理:重复的走访要排序的数列,一次比较两个元素,如果他们的顺序错误就交换。
-
冒泡排序的思想:
比较相邻的元素。如果第一个元素比第二个大(升序),就交换这两个元素。
对每一对相邻元素做同样的操作,从开始第一对到结尾的最后一对。这步完成之后,最后的元素会是最大的数。
除最后一个元素外,针对所有的元素重复上述操作。
持续每次对越来越少的元素重复上面的操作,之到没有任何一对元素需要比较为止。
-
冒泡排序优化:在冒泡排序过程中,各元素不断接近自己的位置,如果,一趟比较下来没有进行过交换,就说明序列有序,因此在排序过程中设置一个标志swap用以判断元素是否进行过交换。从而减少不必要的比较。
-
//冒泡排序 public class BubbleSortTest{ public static void main(String[] args){ int[] arr = new int[]{43,32,76,-98,0,64,33,-21,32,99}; //bubblesort 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; } } } } }
-
-
快速排序算法:快速排序通常明显比同为O(nlogn)的其他算法更快,因此常被采用,而且快速排序采用了分治法的思想。
-
排序算法性能对比
排序方法 时间复杂度(平均) 时间复杂度(最坏) 时间复杂度(最好) 空间复杂度 稳定性 插入排序 O(n²) O(n²) O(n) O(1) 稳定 希尔排序 O(n^1.3) O(n²) O(n) O(1) 不稳定 选择排序 O(n²) O(n²) O(n²) O(1) 不稳定 堆排序 O(nlog₂n) O(nlog₂n) O(nlog₂n) O(1) 不稳定 冒泡排序 O(n²) O(n²) O(n) O(1) 稳定 快速排序 O(nlog₂n) O(n²) O(nlog₂n) O(nlog₂n) 不稳定 归并排序 O(nlog₂n) O(nlog₂n) O(nlog₂n) O(n) 稳定 计数排序 O(n + k) O(n + k) O(n + k) O(n + k) 稳定 桶排序 O(n + k) O(n²) O(n) O(n + k) 稳定 基数排序 O(n * k) O(n * k) O(n * k) O(n + k) 稳定 堆排序、快速排序、归并排序虽然平均时间复杂度相同,但是nlog₂n的系数不同
-
各种内部排序方法的性能比较
- 从平均时间而言:快速排序最佳。但在最坏情况下时间性能不如堆排序和归并排序
- 从算法简单性看:由于直接选择排序、直接插入排序和冒泡排序的算法比较简单,将其 认位是简单算法。对于希尔排序、堆排序、快速排序和归并排序算法,其算法较为复杂,认为是复杂排序
- 从稳定性看:直接插入排序、冒泡排序、和归并排序是稳定的;而直接选择排序、快速排序、希尔排序和堆排序都是不稳定的排序
- 从待排序的记录n的大小来看,n比较小时(n <= 50),宜采用简单排序。当记录规模较小时,直接插入排序较好。否则应选择直接选择排序,因为直接选择移动的记录数少于直接插入。
- 从待排序的记录n的大小来看,n较大时宜采用改进排序。快速排序、堆排序、归并排序
5.Arrays工具类的使用
-
java.util.Arrays类即为操作数组的工具类,包含了用来操作数组的的各种方法(排序、搜索等)
方法 操作 boolean equals(int[] a,int[] b) 判断两个数组是否相等 String toString(int[] a) 格式输出数组信息 void fill(int[] a,int val) 将指定值填充到数组中 void sort(int[] a) 对数组进行排序 int binarySearch(int[] a,int key) 对排序后的数组进行二分法检索指定的值 -
练习
import java.util.Arrays; public class ArraysTest { public static void main(String[] args){ //1. boolean equals(int[] a,int[] b) int[] arr1 = new int[]{1,2,3,4}; int[] arr2 = new int[]{1,3,2,4}; boolean isEquals = Arrays.equals(arr1,arr2); System.out.println(isEquals); //2. String toString(int[] a) System.out.println(Arrays.toString(arr1)); //3. void fill(int[] a,int val) Arrays.fill(arr1,10); System.out.println(Arrays.toString(arr1)); //4. void sort(int[] a) Arrays.sort(arr2); System.out.println(Arrays.toString(arr2)); //5.int binarySearch(int[] a,int key) int[] arr3 = new int[]{-98,-34,2,34,54,66,79,105,210,333}; int index = Arrays.binarySearch(arr3,210); System.out.println(index); if(index >= 0){ System.out.println("找到了"); }else{ System.out.println("没找到"); } } }
6.数组中常见的异常
-
数组角标越界异常:ArrayIndexOutOfBoundsException
int[] arr = new int[]{1,2,3,4,5}; //for(int i = 0;i <= arr.length;i++){ // System.out.print(arr[i] + "\t"); //} //System.out.print(arr[-2]); //一旦程序抛出异常之后,后续的语句不会被执行,但是位于try{}catch{}语句块的结尾的finally{}一定会执行
-
空指针异常:NullPointerException
//情况一: int[] arr1 = new int[]{1,2,3}; arr1 = null; System.out.println(arr1[0]); //相当于在内存的栈区手动将arr1指向堆区的指针设置为null //于是想要调动没有被任何指针指向的arr1[0]会报空指针异常
//情况二: int[][] arr2 = new int[4][]; System.out.println(arr2[0][0]); //arr2[0]存在,但是数组arr2使用的时动态初始化方式二,因此内层元素并没有被赋值 //想要程序打印内层元素时,即会出现空指针异常
//情况三: String[] arr3 = new String[]{"AA","BB","CC"}; arr3[0] = null; System.out.println(arr3[0].toString()); //arr3[0]被重新赋值为null,导致arr3在栈区中找不到位于堆空间的起始元素的位置