一篇学会java数组(包含实例解析+算法实现,建议看后上手实操)
哈喽,大家好!我是Why,一名在读学生,目前刚刚开始进入自己的编程学习生涯。虽然学习起步较晚,但我坚信做了才有0或1的可能。学了一段时间以后也是选择在CSDN上分享自己的日常笔记,也希望能够在众多道友的大家庭中打成一片。
本文近万字,篇幅有点长,主要讲解java中的数组。如果大家读后觉得有用的话,还请大家多多支持博主:欢迎 ❤️点赞👍、收藏⭐、留言💬
✨✨✨个人主页:java-WangHY
文章目录
- 🎯Array
🎯Array
1、Java中的数组是一种引用数据类型,故数组对象是在堆内存中,不属于基本数据类型,其父类是Object,是一种简单的数据结构
2、主观上讲,可以将数组认为是一个容器,它可以同时容纳多个元素,即一个数据的’集合’
3、数组中存储的元素可以是任意类型的,引用数据类型 && 基本数据类型
4、当数组中存储的是’java对象’的时候,相当于存储了该数据的’住址’(即其内存地址),并不能直接将该类型的数据直接存储到数组当中去
5、数组一旦被创建,其长度不可变(Java),有些扩容算法的底层实际上是由一个大的数组(可以想象为容器)进行了替换,而不是真正的扩容
6、所有数组都具有的属性
length,用来获取其长度7、数组的分类:
一维数组,二维数组,三维数组…多维数组8、数组虽然可以存储任意类型的数据,但是java中要求
‘数组中的元素类型必须统一’,比如int类型的数组,不能存储char类型的数据、tudent类型的数组只能存储Student类型的数据。
就像家里的面缸里不能即装面又装水一样,存储的元素必须统一9、数组在存储的时候,其’数组内的元素内存地址都是规则性连续排列的’,所以在创建数组的时候实际上就是在内存中开辟了一块很大的内存
这是数组很重要的特点。10、数组中的每个元素都有下标,但是需要注意的是:
数组的元素下标都是从 0 开始的,逐一递增取值为【0 ~ 数组.length - 1】
‘下标是操作数组最重要的手段’11、数组中有很多数据,但是数组都是以数组中的第一个元素的内存地址当做自己(整个数组对象)的内存地址
Array内存地址 = Array[0]内存地址
🌈怎么样去定义(声明)一个一维数组
定义/声明数组的时候,等于只创建了数组引用名,注意,这样并未在内存中开辟数组空间
数据类型[] 数组名;
int[] array1;
double[] array2;
boolean[] array3;
Object[] array4;
🤙数组的初始化
1、静态初始化
静态初始化语法格式:
数据类型[] 数组名称 = {数组内容 1,数组内容 2,数组内容 3…数组内容 n};
int[] array = {1, 2, 3, 4, 5};2、动态初始化(有默认值)
动态初始化语法格式:
数据类型[] 数组名称 = new 数据类型[数组长度];
int[] array = new int[5]; // 初始化一个5个长度的int类型数组,每个元素默认值0
String[] names = new String[5]; // 初始化5个长度的String类型数组,每个元素默认值null。3、不常用的初始化语法格式:
数据类型[] 数组名称 = new 数据类型[]{内容 1,内容 2,内容 3…内容 n};
int[] array = new int[]{1, 2, 3, 4, 5};什么时候采用静态初始化方式,什么时候使用动态初始化方式呢?
当你创建数组的时候,确定数组中存储哪些具体的元素时,采用静态初始化方式。
当你创建数组的时候,不确定将来数组中存储哪些数据,你可以采用动态初始化的方式,预先分配内存空间。
📈数组的优缺点
💎优点
查询检索效率高
原因:
1、数组内元素数据类型相同,占据的内存空间一样2、数组内每个元素的内存地址在空间上的存储位置是连续的,故知道了一个元素的内存地址就很容易计算出其他的元素的内存地址
所以数组的检索效率几乎不受其大小的影响,因为都可以直接计算的,
知道了第一个元素存储的地址,知道了占用的内存空间大小,可以直接定位到指定下标的内存地址
💎缺点
随机增删效率低,不能存储大量数据
原因:
1、由于数据中的元素的内存地址都是连续的,为了保证这个特点,在进行随机增删数组中的元素的时候
会涉及增删位置其后的元素整体前移或者后移的操作
但是,对于数组最后一个元素的操作,是没有效率影响的2、不能存储大量的元素原因是很难在内存空间上找到一个特别大的连续内存空间
🌾一维数组实例
例子1
public class Test { public static void main(String[] args) { // 使用静态初始化的方式声明一个int类型的数组 int[] a = {1,2,3,4,5}; // 调用数组的length属性,打印数组的长度 System.out.println("数组中元素的个数" + a.length); System.out.println("---------------我是一个分界线OvO-------------------"); // 通过下标对数组中的元素进行存和取(下标从0开始) //读取操作 System.out.println("数组a的第一个元素是--> " + a[0]);//1 System.out.println("数组a的最后一个元素-->" + a[4]);//5 //当数组的长度未知的时候,可以使用 数组名【数组名.length - 1】来获取最后一个元素,这个比较常用 System.out.println("数组a的最后一个元素-->" + a[a.length - 1]); System.out.println("---------------我是一个分界线OvO-------------------"); // 修改操作 // 把第一个元素修改为1314 a[0] = 1314; // 把最后一个元素修改为1024 a[a.length - 1] = 1024; System.out.println("数组a的第一个元素是--> " + a[0]);//1 System.out.println("数组a的最后一个元素-->" + a[4]);//5 System.out.println("---------------我是一个分界线OvO-------------------"); // 一维数组的遍历(即自始至终依次使用),实际上就是遍历下标 //正序遍历 for(int i = 0; i < a.length; i++){ System.out.println("正序遍历输出-->" +a[i]); } System.out.println("---------------我是一个分界线OvO-------------------"); // 逆序遍历到第1个元素 for (int i = a.length - 1; i >= 0; i--) { System.out.println("倒序遍历输出-->" + a[i]); } }}
例子2
public class Test { public static void main(String[] args) { // 采用动态初始化的方式声明(定义)一个一维数组 // 创建长度为5的int数组,数组中每个元素的默认值是0 int[] a = new int[5]; // 遍历数组,此时数组中元素均为默认值 for (int i = 0; i < a.length; i++) { System.out.println("a中下标为" + i + "的元素是-->" + a[i]); } System.out.println("--------------------------我是一个分界线OvO-------------------"); //可以在遍历中赋值 1-5 然后输出赋值后的结果 for (int i = 0; i < a.length; i++) { a[i] = i + 1; System.out.println("a中下标为" + i + "的元素是-->" + a[i]); } System.out.println("--------------------------我是一个分界线OvO-------------------"); //动态初始化方式初始化一个Object类型的数组(很常用*) //默认值是null Object[] objs = new Object[5]; for (int i = 0; i < objs.length; i++) { //相当于 Object obj = objects[i]; //System.out.println(obj); System.out.println(objs[i]); } System.out.println("--------------------------我是一个分界线OvO-------------------"); //for循环遍历的另一种方法foreach for (Object obj : objs) { System.out.println(obj); } System.out.println("--------------------------我是一个分界线OvO-------------------"); // 采用静态初始化的方式初始化一个Object类型的数组 //注意静态的初始化的特点,已确定数组中药存储那些数据,故这些数据必须先存在 Object o1 = new Object(); Object o2 = new Object(); Object o3 = new Object(); //Object[] objects = {o1, o2, o3}; Object[] objects = {new Object(), new Object(), new Object()}; }}
例子3
package com.blog;// 将数组作为一个参数进行传递public class Test { //不要忘记mai方法中的参数也是一个数组 public static void main(String args[]) { //创建一个任意类型(引用数据类型)的数组 Integer[] array1 = {1,2,3,4,5}; printArrays(array1); System.out.println("----------俺是一个分界线QAQ-----------"); String[] array2 = {"abc","def","ghi"}; printArrays(array2); System.out.println("----------俺是一个分界线QAQ-----------"); //还可以这样传递数组 printArrays(new Integer[]{6,6,6,6,6}); //但是没有这种 //printArrays({1,2,3,4}); } / * @param args 参数用Object,方便传输(我不懒) * * 结果:将接收到的数组遍历输出每一个元素 */ public static void printArrays(Object[] objects){ System.out.println("该数组中的元素依次为:"); for(int i = 0; i < objects.length; i++){ System.out.println("第"+(i+1)+"个元素是-->" + objects[i]); } }}
例子4–>测试main函数中的数组参数
//main方法的数组参数 当运行一个方法的时候,底层jvm会优先找到main函数的入口,并传递一个String数组 JVM默认传递的数组长度为 0 (下有证明),这个数组是留给用户的,用户在运行的时候 可以在控制台上输入参数(下有教程),这些参数会被自动转换为main的 String[] args 里面的元素
🏓运行main函数带参数的方法
1、dos窗口 java 类名 参数1 参数2 参数3 ... 参数n(每个参数之间空格相隔)2、以idea为例子 写好一个类点击run点击edit configuration在program arguments 这个框写上要传递的参数点击OK点击运行(shift + F10)
public class Test { public static void main(String[] args) { //无参数直接运行 结果为 0 System.out.println("JVM给传递过来的这个数组的长度是--->" + args.length); System.out.println("---------------wo shi fen ge xian-------------------"); //带参数运行 输入 abc def ghi // 遍历数组 for (String arg : args) { System.out.println(arg); } }}
例子5
数组中的引用数据类型
public class Test { public static void main(String[] args) { //创建boy girl littleBoy 对象 Boy boy1 = new Boy(); Boy boy2 = new Boy(); Girl girl1 = new Girl(); Girl girl2 = new Girl(); LittleBoy littleBoy1 = new LittleBoy(); LittleBoy littleBoy2 = new LittleBoy(); //静态初始化一个boy类型的数组 //这个数组只能存boy类型和 它的子类 Boy[] boys = {boy1,boy2}; //遍历该数组 for (int i = 0;i < boys.length;i++) { //直接输出的话输入的是boy的'地址' System.out.println(boys[i]); //此时取出来的都是一个个boy对象,可以直接调用boy特有的方法 boys[i].play(); //上面这个相当于 Boy boy = (Boy)boys[i]; boy.play(); } System.out.println("---------------------------------------"); //动态初始化一个Boy数组 boy5 Boy[] boy5 = new Boy[2]; //在Boy数组中存储LittleBoy对象,不会报错,但是存储Girl对象就会报错 //Error:(32, 19) java: 不兼容的类型: com.blog.Girl无法转换为com.blog.Boy boy5[0] = littleBoy1; boy5[1] = littleBoy2; //boy5[1] = girl1; //那么当子类重写父类的方法的时候,在进行遍历的时候会优先使用那父类的还是子类重写的方法? //能不能直接调用子类特有的方法? for (int i = 0; i < boy5.length; i++) { //运行结果显示,会优先使用子类重写的方法 boy5[i].play(); //分析为什么不能直接调用子类对象特有的方法?直接调用显示没有这个方法? //取出该对象的时候等于 Boy boy = (Boy)boy5[i]; //相当于将其强转为一个Boy类型 而Boy中显然没有子类特有的方法,故报错 //怎么解决? //向下转型!见下一个for循环 //boy5[i].eat(); } System.out.println("-----------------------------"); for (int i = 0; i < boy5.length; i++) { LittleBoy littleBoy = (LittleBoy)boy5[i]; littleBoy.eat(); } }}class Boy{ public void play(){ System.out.println("I’m a boy,i like playing ! "); }}class Girl{ public void dance(){ System.out.println("I’m a girl,i like dancing ! "); }}class LittleBoy extends Boy{ @Override public void play() { System.out.println("我还小,我不会玩!QAQ!!!"); } public void eat(){ System.out.println("I’m a littleBoy,i like eating ! "); }}
🌾一维数组的扩容
关于一维数组的扩容:在java开发中,数组的长度一旦确定,是不可变的,当需求大于实际开辟的大小的时候就必然涉及到容量的扩增 java中的一维数组是如何扩容的? '就是以大代小,先创建一个大容量的数组,再讲小容量数组中的数据一个个拷贝进去' System中提供了一个静态(native)方法 arraycopy(): System.arraycopy(Object src,(原数组) int srcPos,(原数组要复制的起始位置) Object dest,(目标数组) int destPos,(目标数组的起始位置) int length)(要复制的长度)要注意: 当数组为'一维数组' 且 '存储的数据类型为非基本数据类型或者String类型的时候' 该方法会将 '象的值'和 '对象的内容' 进行复制 【 String是因为其不可变性 】 即修改副本中的数据,不会对原有的数据产生影响 又叫深度复制 当一维数组中存储的数据为'引用类型数据或者数组为多维数组'的时候 arraycopy的结果是将原数组中'对象的引用进行复制' 即修改副本中的数据,会对原有的数据产生影响,因为原数组与新数组的数据引用指向同一个对象 又叫浅度复制
例子:
public class Test { public static void main(String[] args) { // 定义一个原数组 int[] src = {1, 2, 3, 4, 5}; // 定义一个目标数组 int[] dest = new int[10]; //使用arraycopy方法,完成数组的完全拷贝 System.arraycopy(src, 0, dest, 0, src.length); // 遍历目标数组 for (int i = 0; i < dest.length; i++) { System.out.println(dest[i]); } // 数组中如果存储的元素是引用 System.out.println("----------------------------------"); /* * 当数组中存放的是对象的时候 * */ Object[] objs = {new Object(), new Object(), new Object(), new Object(), new Object()}; Object[] newObjs = new Object[10]; System.arraycopy(objs, 0, newObjs, 0, objs.length); for (int i = 0; i < newObjs.length; i++) { System.out.println(newObjs[i]); } }}
区分深度和浅度复制
package com.blog;public class Test { public static void main(String[] args) { / * 当数据为非引用类型时 * */ int[] src1 = {1, 2, 3, 4, 5}; int[] dest1 = new int[10]; //copy数组 System.arraycopy(src1, 0, dest1, 0, src1.length); //遍历数组(只打印有数据的内容便于对比) for (int i = 0; i < 5; i++) { System.out.println("src1中第"+(i+1)+"个元素是-->"+src1[i]+" dest1中第"+(i+1)+"个元素是-->"+dest1[i]); } //分别修改src1以及dest1中元素的值,观察影响 src1[0] = 11; dest1[1] = 22; //再次遍历数组(只打印有数据的内容便于对比) //可以发现两个数组相互独立,修改一个数组的值不会影响另一个 System.out.println("----------------------------------"); for (int i = 0; i < 5; i++) { System.out.println("src1中第"+(i+1)+"个元素是-->"+src1[i]+" dest1中第"+(i+1)+"个元素是-->"+dest1[i]); } // 数组中如果存储的元素是引用 System.out.println("----------------------------------"); / * 当数组中存放的是对象的时候 * */ Student[] stus = {new Student(1,"Jack"), new Student(2,"Michale"), new Student(3,"Maria")}; Student[] newStus = new Student[5]; System.arraycopy(stus, 0, newStus, 0, stus.length); //修改副本中第一个元素的数据 newStus[0].setName("我被修改了!"); //输出原数组的数据,查看是否被修改 System.out.println(stus[0]); }}class Student{ / * id */ private Integer id; / * name */ private String name; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Student() { } public Student(Integer id, String name) { this.id = id; this.name = name; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + '}'; }}
🐳多维数组
在了解java中的多维数组之前,先了解一下java中的二维数组。什么是二维数组呢? 二维数组其实是一个特殊的一维数组,特殊在这个一维数组当中的每一个元素是一个一维数组。那么什么是三维数组呢? 三维数组是一个特殊的二维数组,特殊在这个二维数组中每一个元素是一个一维数组。....... 那么依次类推:'n维数组就是一个特殊的 n-1 维数组,'下面以二维数组为例子进行讲解
🌈二维数组的(声明)定义
和一维数组一样,定义(声明)数组的时候,等于只创建了数组引用名。注意,这样'并未在内存中开辟数组空间' 数据类型[][] 数组名;//其他多维数组的声明方式也一样 int[][] array1; double[][] array2; boolean[][] array3; Object[][] array4;
🤙二维数组的初始化
'静态初始化语法格式' 数据类型[][] 数组名称 = { {数组1},//每个一维数组长度随意 {数组2}, {数组3}, ...,{数组n}}; int[] array = { {1,1}, {2, 2, 2}, ...., {n, n, n, n, ......} };'动态初始化语法格式' 数据类型[][] 数组名称 = new 数据类型[一维数组个数(必须写)][一维数组的长度(可不写)];这里一维数组的长度写上表示固定长度,不写表示不定长'注意:动态初始化多维数组的时候,必须定义其长度,即第一个[]中必须有数字'int[] array = new int[3][5]; // 初始化一个含有3个一维数组,每个一维数组长度为5的 int类型数组,每个元素默认值0String[] names = new String[5][]; //初始化含有5个一维数组,每个一维数组长度 待定 的String类型数组,每个元素默认值null。
例子
public class Test { public static void main(String[] args) { //静态初始化定义一个二维数组 int[][] a = { {1,2,3}, {1,2,3,4,5}, {1,2,3,4,5,6} }; System.out.println(a.length);//3 System.out.println(a[0].length);//3 System.out.println(a[1].length);//5 System.out.println(a[2].length);//6 System.out.println("---------------------------------"); //动态初始化一个二维数组 int[][] array = new int[3][4]; System.out.println(array.length);//3 System.out.println(array[0].length);//4 System.out.println(array[1].length);//4 System.out.println(array[2].length);//4 }}
📈二维数组中元素使用
a[二维数组中的一维数组的下标][一维数组的下标]: a[0][0]:表示第1个一维数组中的第1个元素。 a[3][4]:表示第3个一维数组中的第4个元素。
二维数组的遍历
public class Test { public static void main(String[] args) { //定义一个二维数组 String[][] array = { {"Jack", "Michale", "Maria","Jane"}, {"张三", "李四", "王五","麻子"} }; //遍历二维数组 for (int i = 0; i < array.length; i++) { //依次取出每一个一维数组 String [] s = array[i]; //遍历该一维数组 System.out.println("第"+(i+1)+"组数据:"); for (int j = 0; j < s.length; j++) { //不换行输出,便于阅读 System.out.print(s[j] + " "); } //换行 System.out.println(); } System.out.println("-----合并后结果-------"); //将两个循环合并 for (int i = 0; i < array.length; i++) { System.out.println("第"+(i+1)+"组数据:"); for (int j = 0; j < array[i].length; j++) { System.out.print(array[i][j] + " "); } System.out.println(); } }}
🌾数组最容易出现的错误
1、空指针异常 'NullPointException'原因:数组未赋值2、下标越界 'OutOfBoundsException'原因:使用超出范围的下标,数组下标从0开始,到数组的长度 - 1 结束,且数组一旦创建长度不变
🍎数组常用算法
🥝冒泡排序
冒泡排序算法思想:
对有的数字进行循环比较,用左边的数字和右边的数字进行比较,当左边数字 > 右边数字的时候,交换两个数字的位置,然后进行下一次比较,直至本次循环比较结束;每一次循环结束之后,都会找出最大的数据元素,放到参与比较的这堆数据的最右边,就像冒泡一样,最大的气泡最先冒出。
public class Test { public static void main(String[] args) { //静态初始化一个一维数组 int[] a = {1,2,3,4,4,4,4,23,32,32,432}; //外层循环控制轮数 for (int i = 0; i < a.length - 1; i++) { //内层循环控制比较次数 for (int j = 0; j < a.length - i - 1; j++) { if(a[j] > a[j+1]){ //交换两个数的位置 int temp = a[j]; a[j] = a[j+1]; a[j+1] = temp; } } } //遍历最终结果 for (int i = 0; i < a.length; i++) { System.out.println(a[i]); } }}
🎽选择排序
选择排序思想:选排每次会在'参与比较的数据'当中选择出最小值然后将其和这堆参与比较的数据中最前面的数据交换位置 每一次排序都会筛选出最小值,每一次的交换位置都是有意义的 即第一次比较,会筛选出所有数据中最小的,第二次筛选出第二小的,依次类推

public class Test { public static void main(String[] args) { //静态初始化一个一维数组 int[] a = {1,2,3,4,4,4,4,23,32,32,432}; //外层循环控制轮数 for (int i = 0; i < a.length - 1; i++) { //内层循环控制比较次数 //记录参与比较的这组元素"最小值"的下标,用于 int min = i; for (int j = i + 1; j < a.length; j++) { //更新最小值下标 if(a[j] < a[min]){ min = j; } } //判断最小值的下标是否发生变化,如果有,则更新下标,否则进入下一组循环 if(min != i){ int temp = a[min]; a[min] = a[i]; a[i] = temp; } } //遍历最终结果 for (int i = 0; i < a.length; i++) { System.out.println(a[i]); } }}
🧣二分查找
二分查找是'基于排序'好的元素进行的查找方法二分原理: 1、记录参与比较的中间元素 mid = (begin + end) / 2 2、判断中间值是否符合某种条件,进行区间的更新第一种更新方式:begin = mid,end = mid - 1,此时,mid = (begin + end + 1) / 2;这里加一的原因是为了防止java中的除法向下取整而导致左右区间更新时陷入循环的问题(见下图)第二种更新方式:end = mid,begin = mid + 1,此时,mid = (begin + end) / 2;
package com.blog;import java.util.Scanner;public class Test { / * 该方法仅仅处理无重复元素的有序数组 * 有重复元素的查找范围应该是一个区间 * 在下个例子中实现此功能 * */ public static void main(String[] args) { //定义一个有序数组(无序数组可以先写一个排序算法,再进行二分) int[] array = {1,2,3,4,5,6,7,8,9}; //定义左右边界 int begin = 0,end = array.length - 1; //mid表示区间中间的元素,find表示需要查找的元素 int mid,find; Scanner input = new Scanner(System.in); find = input.nextInt(); //当区间左边界小于右边界的时候,表示已经陷入循环,此时应该终止并退出 while (begin < end){ //更新查询区间注意此处更新区间操作(上述有原因) mid = (begin + end) >> 1; if(array[mid] >= find) { end = mid; }else{ begin = mid + 1; } } //二分一定是有解的,但是题目可能是无解的,所以要进行判断 //由于数组不含重复元素,所以最终结果begin一定是等于end的,所以此处下标选择任一都可以 if(array[begin] != find){ System.out.println("查询无果!"); }else{ System.out.println(end); } }}
package com.blog;import java.util.Scanner;public class Test { / * 该方法仅仅处理含重复元素 * 有重复元素的查找范围应该是一个区间 * */ public static void main(String[] args) { //定义一个有序数组(无序数组可以先写一个排序算法,再进行二分,注意,二分一定是针对有序数组的) int[] array = {1,2,3,4,4,5,5,6,7,8,9}; //定义左右边界 int begin = 0,end = array.length - 1; //mid表示区间中间的元素,find表示需要查找的元素 int mid,find; Scanner input = new Scanner(System.in); find = input.nextInt(); //处理左边界 while (begin < end){ //更新查询区间注意此处更新区间操作 mid = (begin + end) >> 1; if(array[mid] >= find) { end = mid; }else{ begin = mid + 1; } } //判断左边界 if(array[begin] != find){ System.out.println("查询无果!"); }else{ System.out.print("左边界为" + begin); //处理右边界,注意此处是处理右边界,要将右区间进行还原 end = end = array.length - 1; while (begin < end){ //更新查询区间注意此处更新区间操作 mid = (begin + end + 1) >> 1; if(array[mid] <= find) { begin = mid; }else{ end = mid - 1; } } System.out.print("右边界为" + begin); } }}
🤡写在最后
图画的有点丑,但是十分清晰易懂,大家看完之后可以尝试按照博主的思路去上尝试手动实现数组的扩容并尝试实现数组的几种查找算法,自己动手实操才是真正的王道,只有不断地出错不断地尝试,才有不断的进步。
另外,我自己整理了一些自资源(笔记、书籍、软件等)分享在我的公众号上,非常欢迎大家来访白嫖和博主做朋友,一起学习进步!最后别忘啦支持一下博主哦,求三连!❤️❤️❤️