说明:
(1)本篇博客介绍数组,通过基于Java提供的数组,进行二次封装的过程,来加深对数组这种基本的数据结构的理解;
(2)本篇博客涉及的代码,还是比价容易理解的;重要的是,对这个封装的思路、数据结构的套路有个整体的感知;
目录
一:为什么要学习数据结构;
1.数据结构是什么;
●这儿主要突出的是【高效】;
2.数据结构,三种结构;
● 可以通过邻接矩阵或邻接表,来存储图结构数据;;其实,邻接矩阵本质就是一个二维数组,邻接表本质就是一个链表的数组;
● 本专栏,会涉及图论领域的算法实现,而不会介绍图这种数据结构的本身的实现;;;即,本专栏不会介绍图这种数据结构;
● 我们在实际开发阿忠,需要根据具体应用场景和业务的不同,灵活选择最合适的数据结构;
3.数据结构,应用案例;
在【一:线性查找法:2:算法和数据结构有什么用?】也已经介绍过;
(1)数据库;
● 数据库就是用来存储数据的,所以在设计数据库的时候,一定会使用到数据结构;;只是,对于诸如MySQL这种数据库来说,我们是在外存(比如硬盘等)上作数据存储;;;;;然后,本专栏中介绍数据结构时,都是在内存上操作的;
(2)操作系统;
● 优先队列,内存管理,文件管理:这些功能本身,其实就是“组织和存储数据”;
(3)文件压缩;
● 文件压缩是一种算法,而对于很多算法,我们要想执行它,就需要选择合理的数据结构去支撑;
● 比如,一种基础的压缩算法就需要使用哈夫曼树;
(4)游戏;
● 比如游戏中的寻路算法;;我们可以借助DFS(深度优先遍历)或BFS(广度优先遍历)算法来实现;
在本专栏中,我们主要关注的是【在内存中世界中的增删改查】;但是,不要忘记数据结构的基础是【对数据的组织】;
二:Java中的数组;
1.数组,基础;
(1)在Java语言中,要求数组中不同索引上的数据类型必须统一;;;在有的语言中,则没这个规定;
(2)在实际业务开发中,数组的名字一般起一个可以见名知意的名字;
2.数组,演示;
public class Test1 { public static void main(String[] args) { //案例1:定义数组时,需要指定数组的大小; int[] arr = new int[10]; for (int i = 0; i < arr.length; i++) { arr[i] = i; } //案例2:定义数组的第二种方式;在声明的时候,直接初始化; int[] scores1 = new int[]{100, 99, 96}; for (int i = 0; i < scores1.length; i++) { System.out.println(scores1[i]); } //案例3:案例2的简写形式,在实际开发中自己也使用这种简写方式; int[] scores2 = {8, 9, 10}; for (int i = 0; i < scores2.length; i++) { System.out.println(scores2[i]); } //案例4:java中的数组是可迭代的; int[] scores3 = {11, 12, 13}; for (int score : scores3) {//java中的数组,有可遍历(或者说可迭代)的能力; System.out.println(score); } } }
(1)有关Java中的数组,可以参考【Java一维数组:创建、初始化;增强for循环;冒泡排序】;
插:数组补充简介;
1.数组的索引,可以有语义,也可以没语义;
(1)数组的索引,可以有语义,也可以没语义;
● 索引是可以有语义的:比如存储学生的数组,如果把学号作为索引;那么,scores[2]就表示学号为2的那个学生;
● 自然,索引是可以没有语义的;
(2)单纯从【充分利用数组优点】的技术角度,来说,索引有语义是很好的;
● 比如,我们想查学号为10046的学生,直接scores[10046]就行了,十分方便;
(3)但是,很多场景中,不应该使用有语义的索引;
● 我们为了以身份证号为索引,那么我们就要开辟很大的空间;对于计算机来说,这是不值当的、甚至是不可能的;
● 而且,空间开辟那么多,也很可能会造成浪费;;;比如,目前就有10个人;如果以IDNumber为索引的话,就会造成很大很大的浪费;
● 而且,自己在实际开发中,使用数组时,一般索引都没有语义;
2.Java提供的数组,不支持扩容和缩容;
Java提供的数组,不支持扩容和缩容(即,Java提供的数组是静态数组);为此,我们要基于Java提供的数组,进行二次封装,得到一个动态数组;
三:基于Java提供的数组,二次封装,得到自己的数组;
import com.sun.org.apache.xpath.internal.objects.XNull; import sun.awt.windows.WingDings; public class Array { private int[] data; private int size;//数组中,实际元素个数;;;capacity我们就不定义了,因为data.length就是; /** * 自己写一个构造函数,传入数组容量capacity,去创建一个数组; * @param capacity */ public Array(int capacity) { data = new int[capacity]; size = 0; } /** * 定义无参构造 */ public Array() { this(10);//如果用户在创建Array时,没有传入参数,那么我们就默认其capacity是10; } /** * 查看数组中,元素个数,即查看size; * @return */ public int getSize() { return size; } /** * 查看数组容量,即查看capacity; * @return */ public int getCapacity() { return data.length; } /** * 数组是否为空; * * @return */ public boolean isEmpty() { return size == 0; } /** * 向数组中追加一个元素; * 比如一个数组capacity是10,目前size是6;那么我们就是添加第7个元素,其实也就是在索引为6的位置上添加一个元素; * 比如数组原先是:66 88 99 100 0 0 0;;我们想要追加77,变成66 88 99 100 77 0 0; * * @param e */ public void addLast(int e) { //如果此时,数组已经满了,就抛出一个异常; if (size == data.length) { throw new IllegalArgumentException("AddLast failed. Array is full."); } data[size] = e; size++;//自然,添加完元素后,数组的size要记得加1; // add(size, e);//我们开发了add()方法后,其实这儿就可以使用add()方法; } /** * 向数组指定位置,添加一个元素; * 比如数组原先是:66 88 99 100 0 0 0;;我们想要在index=1的位置上添加77,变成66 77 88 99 100 0 0; * @param index * @param e */ public void add(int index,int e) { //如果此时,数组已经满了,就抛出一个异常; if (size == data.length) { throw new IllegalArgumentException("AddLast failed. Array is full."); } //要求,索引不能为负;索引不能大于size,否则就会出现66 88 99 100 0 77 0这种情况了,这与【数组中的元素需要连续】相违背; if (index < 0 || index > size) { throw new IllegalArgumentException("AddLast failed. Require index < 0 and index > size."); } //把88 99 100都向后平移一位; for (int i = size - 1; i >= index; i--) { data[i + 1] = data[i]; } data[index] = e; size++;//自然,添加完元素后,数组的size要记得加1; } /** * 向数组第一个位置,添加一个元素; * 比如数组原先是:66 88 99 100 0 0 0;;我们想要在i数组第一个位置上添加77,变成77 66 88 99 100 0 0; * @param e */ public void addFirst(int e) { add(0, e);//这儿就可以直接调用add()方法了; } /** * 根据索引,获取数据 * * @param index * @return */ public int get(int index) { // (1)已经知道,如果数组capacity=10,然后目前数组只存了3个数,即size=3;;;那么此时这个数组是没有存满的;通 // 过get方法的逻辑,可以看到,用户是永远无法访问到那些还没存元素的索引位置的; // (2)而且,还可以发现,我们在Array类中定义的data[]数组,对外是不可见的;;即用户只能感受到Array,而感受不到我 // 们底层利用了data[]的; if (index < 0 || index >= size) { throw new IllegalArgumentException("Get failed. Index is illegal."); } return data[index]; } /** * 根据所有,修改某个元素;(注意是修改,而不是新增) * @param index * @param e * @return */ public void set(int index,int e) { // 已经知道,如果数组capacity=10,然后目前数组只存了3个数,即size=3;;;那么此时这个数组是没有存满的; // 通过set方法的逻辑,可以看到,用户是永远无法修改到那些还没存元素的索引位置的; if (index < 0 || index >= size) { throw new IllegalArgumentException("Get failed. Index is illegal."); } data[index] = e; } /** * 判断,数组中是否包含某个元素; * @param e * @return */ public boolean contains(int e) { for (int i = 0; i < size; i++) { if (data[i] == e) { return true; } } return false; } /** * 判断,数组中是否包含某个元素;如果有,返回这个元素在数组中的索引;(其实,这儿查询的是第一个;比如数组中有三个位置都是77, * 如果我们查找77的话,返回的是第一个的索引) * @param e * @return */ public int find(int e) { for (int i = 0; i < size; i++) { if (data[i] == e) { return i; } } return -1; } /** * 从数组中,删除指定索引的元素,并返回该元素的值; * 比如一个数组是:66 77 88 99 100 0 0 0,我们要删除index=1那个元素,那么删除后就是:66 88 99 100 0 0 0 0; * 然后返回77; * @param index * @return */ public int remove(int index) { //先判断index是否OK,这其中自然涵盖了数组是否为空,索引是否过大等情况; if (index < 0 || index >= size) { throw new IllegalArgumentException("Delete failed. Index is illegal."); } int ret = data[index]; for (int i = index + 1; i < size; i++) { data[i-1] = data[i]; } size--;//别忘了,维护一下size; return ret; //data[size] = 0;//其实,我们可以不做一步操作,因为我们的编程实现,用户根本访问不到index=size及之后的位置; } /** * 删除第一个元素,并返回这个删除的元素; * @return */ public int removeFirst() { return remove(0); } /** * 删除最后一个元素,并返回这个删除的元素; * @return */ public int removeLast() { return remove(size - 1); } /** * 判断数组中是否包含某个元素,如果有就删除;(其实,这儿删除的是第一个;比如数组中有三个位置都是77,如果我们删除77的话, * 删除的是第一个) * @param e */ public void removeElement(int e) { int index = find(e); if (index != -1) { remove(index); } } /** * 判断数组中是否包含某个元素,如果有就删除;同时,如果有,而且我们删除了的话,就返回true;如果没有就返回false; * @param e * @return */ public boolean removeElementIsOK(int e) { int index = find(e); if (index != -1) { remove(index); return true; } return false; } /** * 重写toString方法,打印数组的基本信息和元素内容; * @return */ @Override public String toString() { StringBuilder res = new StringBuilder(); res.append(String.format("Array: size = %d, capacity = %d\n", size, data.length)); res.append('['); for (int i = 0; i < size; i++) { res.append(data[i]); if (i != size - 1) { res.append(','); } } res.append(']'); return res.toString(); } }
说明:
(1)上面是基于Java的数组,封装自己的数组;内容挺好理解的,看注释;
(2)但是,上面的数组还有两个需要改进的地方:
● 目前Array只能是int类型的,后面要支持泛型;
● 数组无法扩容和缩容,后面需要动态数组;
(3)也可以测试;
public class Test2 { public static void main(String[] args) { Array array = new Array(20); for (int i = 0; i < 10; i++) { array.addLast(i); } System.out.println(array); array.add(1, 100); System.out.println(array); array.addFirst(9); System.out.println(array); array.remove(2); System.out.println(array); //………… } }
四:改进1:支持泛型的数组;
(1)数组应该能够盛放int,String这种java原本就有的类型;也要能够存放自定义的,如Students这种类型;
(2)为此,就需要把上面的Array类,设为泛型类;有关泛型的内容,如有需要可以参考【Java泛型二:自定义泛型类】及附近相关文章;
自定义支持泛型的数组;
/** * 支持泛型 */ public class Array2<E> { //Array2盛放的数据类型是E,那么具体E是什么,我们在使用Array2的时候再声明; private E[] data; private int size;//数组中,实际元素个数;;;capacity我们就不定义了,因为data.length就是; /** * 自己写一个构造函数,传入数组容量capacity,去创建一个数组; * @param capacity */ public Array2(int capacity) { // data = new E[capacity];//java本身不支持:"new 泛型类型[]",这种直接new一个泛型数组;这条语句会报错; data = (E[]) new Object[capacity];//因为,上面会报错,所以对于java来说,我们需要这么做; size = 0; } /** * 定义无参构造 */ public Array2() { this(10);//如果用户在创建Array时,没有传入参数,那么我们就默认其capacity是10; } /** * 查看数组中,元素个数,即查看size; * @return */ public int getSize() { return size; } /** * 查看数组容量,即查看capacity; * @return */ public int getCapacity() { return data.length; } /** * 数组是否为空; * * @return */ public boolean isEmpty() { return size == 0; } /** * 向数组中追加一个元素; * 比如一个数组capacity是10,目前size是6;那么我们就是添加第7个元素,其实也就是在索引为6的位置上添加一个元素; * 比如数组原先是:66 88 99 100 0 0 0;;我们想要追加77,变成66 88 99 100 77 0 0; * * @param e */ public void addLast(E e) { //如果此时,数组已经满了,就抛出一个异常; if (size == data.length) { throw new IllegalArgumentException("AddLast failed. Array is full."); } data[size] = e; size++;//自然,添加完元素后,数组的size要记得加1; // add(size, e);//我们开发了add()方法后,其实这儿就可以使用add()方法; } /** * 向数组指定位置,添加一个元素; * 比如数组原先是:66 88 99 100 0 0 0;;我们想要在index=1的位置上添加77,变成66 77 88 99 100 0 0; * @param index * @param e */ public void add(int index,E e) { //如果此时,数组已经满了,就抛出一个异常; if (size == data.length) { throw new IllegalArgumentException("AddLast failed. Array is full."); } //要求,索引不能为负;索引不能大于size,否则就会出现66 88 99 100 0 77 0这种情况了,这与【数组中的元素需要连续】相违背; if (index < 0 || index > size) { throw new IllegalArgumentException("AddLast failed. Require index < 0 and index > size."); } //把88 99 100都向后平移一位; for (int i = size - 1; i >= index; i--) { data[i + 1] = data[i]; } data[index] = e; size++;//自然,添加完元素后,数组的size要记得加1; } /** * 向数组第一个位置,添加一个元素; * 比如数组原先是:66 88 99 100 0 0 0;;我们想要在i数组第一个位置上添加77,变成77 66 88 99 100 0 0; * @param e */ public void addFirst(E e) { add(0, e);//这儿就可以直接调用add()方法了; } /** * 根据索引,获取数据 * * @param index * @return */ public E get(int index) { // (1)已经知道,如果数组capacity=10,然后目前数组只存了3个数,即size=3;;;那么此时这个数组是没有存满的;通 // 过get方法的逻辑,可以看到,用户是永远无法访问到那些还没存元素的索引位置的; // (2)而且,还可以发现,我们在Array类中定义的data[]数组,对外是不可见的;;即用户只能感受到Array,而感受不到我 // 们底层利用了data[]的; if (index < 0 || index >= size) { throw new IllegalArgumentException("Get failed. Index is illegal."); } return data[index]; } /** * 根据所有,修改某个元素;(注意是修改,而不是新增) * @param index * @param e * @return */ public void set(int index,E e) { // 已经知道,如果数组capacity=10,然后目前数组只存了3个数,即size=3;;;那么此时这个数组是没有存满的; // 通过set方法的逻辑,可以看到,用户是永远无法修改到那些还没存元素的索引位置的; if (index < 0 || index >= size) { throw new IllegalArgumentException("Get failed. Index is illegal."); } data[index] = e; } /** * 判断,数组中是否包含某个元素; * @param e * @return */ public boolean contains(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) { //改为E之后,这儿就是比较对象了,我么就不能使用"if (data[i] == e)"了; return true; } } return false; } /** * 判断,数组中是否包含某个元素;如果有,返回这个元素在数组中的索引;(其实,这儿查询的是第一个;比如数组中有三个位置都是77, * 如果我们查找77的话,返回的是第一个的索引) * @param e * @return */ public int find(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) {//改为E之后,这儿就是比较对象了,我么就不能使用"if (data[i] == e)"了; return i; } } return -1; } /** * 从数组中,删除指定索引的元素,并返回该元素的值; * 比如一个数组是:66 77 88 99 100 0 0 0,我们要删除index=1那个元素,那么删除后就是:66 88 99 100 0 0 0 0; * 然后返回77; * @param index * @return */ public E remove(int index) { //先判断index是否OK,这其中自然涵盖了数组是否为空,索引是否过大等情况; if (index < 0 || index >= size) { throw new IllegalArgumentException("Delete failed. Index is illegal."); } E ret = data[index]; for (int i = index + 1; i < size; i++) { data[i-1] = data[i]; } size--;//别忘了,维护一下size; //data[size] = 0;//其实,我们可以不做一步操作,因为我们的编程实现,用户根本访问不到index=size及之后的位置; //但是,如果E是一个复杂对象的话,因为data[size]因为有应用,所以其不会被垃圾回收,这就涉及到了空间释放的问题; //所以,此时,可以增加下面一句话,释放空间;;;;自然,不加也行; //PS:对于这种data[size]处的对象,有个术语loitering objects,即闲逛的对象;即这个对象本身已经没有用了,但其并 // 没有被回收,还在程序内存中;;;;但是loitering objects并不代表memory leak内存泄露; data[size] = null; return ret; } /** * 删除第一个元素,并返回这个删除的元素; * @return */ public E removeFirst() { return remove(0); } /** * 删除最后一个元素,并返回这个删除的元素; * @return */ public E removeLast() { return remove(size - 1); } /** * 判断数组中是否包含某个元素,如果有就删除;(其实,这儿删除的是第一个;比如数组中有三个位置都是77,如果我们删除77的话, * 删除的是第一个) * @param e */ public void removeElement(E e) { int index = find(e); if (index != -1) { remove(index); } } /** * 判断数组中是否包含某个元素,如果有就删除;同时,如果有,而且我们删除了的话,就返回true;如果没有就返回false; * @param e * @return */ public boolean removeElementIsOK(E e) { int index = find(e); if (index != -1) { remove(index); return true; } return false; } /** * 重写toString方法,打印数组的基本信息和元素内容; * @return */ @Override public String toString() { StringBuilder res = new StringBuilder(); res.append(String.format("Array: size = %d, capacity = %d\n", size, data.length)); res.append('['); for (int i = 0; i < size; i++) { res.append(data[i]); if (i != size - 1) { res.append(','); } } res.append(']'); return res.toString(); } }
说明:
(1)泛型类不难理解,挺简单的;上面的改造过程也挺简单的,看注释;
(2)需要注意两点:
● Java语言,不允许直接创建泛型数组;(这算是一个有争议的,遗留问题)
● loitering objects,闲逛对象(或游荡对象);
五:改进2:动态数组;(数组可以扩容和缩容)
自然,当删除数组中的元素,删除到一定程度的时候,也可以采取类似的思路来缩容;
可以扩容的动态数组;
import javafx.scene.chart.PieChart; /** * 支持泛型 */ public class Array3<E> { //Array2盛放的数据类型是E,那么具体E是什么,我们在使用Array2的时候再声明; private E[] data; private int size;//数组中,实际元素个数;;;capacity我们就不定义了,因为data.length就是; /** * 自己写一个构造函数,传入数组容量capacity,去创建一个数组; * @param capacity */ public Array3(int capacity) { // data = new E[capacity];//java本身不支持:"new 泛型类型[]",这种直接new一个泛型数组;这条语句会报错; data = (E[]) new Object[capacity];//因为,上面会报错,所以对于java来说,我们需要这么做; size = 0; } /** * 定义无参构造 */ public Array3() { this(10);//如果用户在创建Array时,没有传入参数,那么我们就默认其capacity是10; } /** * 查看数组中,元素个数,即查看size; * @return */ public int getSize() { return size; } /** * 查看数组容量,即查看capacity; * @return */ public int getCapacity() { return data.length; } /** * 数组是否为空; * * @return */ public boolean isEmpty() { return size == 0; } /** * 向数组中追加一个元素; * 比如一个数组capacity是10,目前size是6;那么我们就是添加第7个元素,其实也就是在索引为6的位置上添加一个元素; * 比如数组原先是:66 88 99 100 0 0 0;;我们想要追加77,变成66 88 99 100 77 0 0; * * @param e */ public void addLast(E e) { // //如果此时,数组已经满了,就抛出一个异常; // if (size == data.length) { // throw new IllegalArgumentException("AddLast failed. Array is full."); // } // // data[size] = e; // size++;//自然,添加完元素后,数组的size要记得加1; add(size, e);//我们开发了add()方法后,其实这儿就可以使用add()方法; } /** * 向数组指定位置,添加一个元素; * 比如数组原先是:66 88 99 100 0 0 0;;我们想要在index=1的位置上添加77,变成66 77 88 99 100 0 0; * @param index * @param e */ public void add(int index,E e) { //要求,索引不能为负;索引不能大于size,否则就会出现66 88 99 100 0 77 0这种情况了,这与【数组中的元素需要连续】相违背; if (index < 0 || index > size) { throw new IllegalArgumentException("AddLast failed. Require index < 0 and index > size."); } //如果此时,数组已经满了,就去扩容; if (size == data.length) { //当数组已经满了,需要扩容的时候,这儿采取的策略是,在原有长度的基础上扩大一倍;(Java提供的动态数组ArrayList是扩1.5倍) //这儿我们之所以不扩容一个常数(比如扩容10个长度)的原因是:数组的实际存储情况不同,这个常数不好确定;比如数组容 // 量是一万,如果扩容10的话,就有点不痛不痒、杯水车薪;;如果数组容量是10的话,如果扩容1000,就会太猛了; resize(2 * data.length); } //把88 99 100都向后平移一位; for (int i = size - 1; i >= index; i--) { data[i + 1] = data[i]; } data[index] = e; size++;//自然,添加完元素后,数组的size要记得加1; } /** * 向数组第一个位置,添加一个元素; * 比如数组原先是:66 88 99 100 0 0 0;;我们想要在i数组第一个位置上添加77,变成77 66 88 99 100 0 0; * @param e */ public void addFirst(E e) { add(0, e);//这儿就可以直接调用add()方法了; } /** * 根据索引,获取数据 * * @param index * @return */ public E get(int index) { // (1)已经知道,如果数组capacity=10,然后目前数组只存了3个数,即size=3;;;那么此时这个数组是没有存满的;通 // 过get方法的逻辑,可以看到,用户是永远无法访问到那些还没存元素的索引位置的; // (2)而且,还可以发现,我们在Array类中定义的data[]数组,对外是不可见的;;即用户只能感受到Array,而感受不到我 // 们底层利用了data[]的; if (index < 0 || index >= size) { throw new IllegalArgumentException("Get failed. Index is illegal."); } return data[index]; } /** * 根据所有,修改某个元素;(注意是修改,而不是新增) * @param index * @param e * @return */ public void set(int index,E e) { // 已经知道,如果数组capacity=10,然后目前数组只存了3个数,即size=3;;;那么此时这个数组是没有存满的; // 通过set方法的逻辑,可以看到,用户是永远无法修改到那些还没存元素的索引位置的; if (index < 0 || index >= size) { throw new IllegalArgumentException("Get failed. Index is illegal."); } data[index] = e; } /** * 判断,数组中是否包含某个元素; * @param e * @return */ public boolean contains(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) { //改为E之后,这儿就是比较对象了,我么就不能使用"if (data[i] == e)"了; return true; } } return false; } /** * 判断,数组中是否包含某个元素;如果有,返回这个元素在数组中的索引;(其实,这儿查询的是第一个;比如数组中有三个位置都是77, * 如果我们查找77的话,返回的是第一个的索引) * @param e * @return */ public int find(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) {//改为E之后,这儿就是比较对象了,我么就不能使用"if (data[i] == e)"了; return i; } } return -1; } /** * 从数组中,删除指定索引的元素,并返回该元素的值; * 比如一个数组是:66 77 88 99 100 0 0 0,我们要删除index=1那个元素,那么删除后就是:66 88 99 100 0 0 0 0; * 然后返回77; * @param index * @return */ public E remove(int index) { //先判断index是否OK,这其中自然涵盖了数组是否为空,索引是否过大等情况; if (index < 0 || index >= size) { throw new IllegalArgumentException("Delete failed. Index is illegal."); } E ret = data[index]; for (int i = index + 1; i < size; i++) { data[i-1] = data[i]; } size--;//别忘了,维护一下size; //data[size] = 0;//其实,我们可以不做一步操作,因为我们的编程实现,用户根本访问不到index=size及之后的位置; //但是,如果E是一个复杂对象的话,因为data[size]因为有应用,所以其不会被垃圾回收,这就涉及到了空间释放的问题; //所以,此时,可以增加下面一句话,释放空间;;;;自然,不加也行; //PS:对于这种data[size]处的对象,有个术语loitering objects,即闲逛的对象;即这个对象本身已经没有用了,但其并 // 没有被回收,还在程序内存中;;;;但是loitering objects并不代表memory leak内存泄露; data[size] = null; //当我们删除数组中元素的时候,如果数组此时实际数据数量为数组容量的一般时,我们就执行缩容操作; if (size == data.length/2) { //而具体,我们可以把数组的capacity缩小为原来的一半; resize(data.length/2); } return ret; } /** * 删除第一个元素,并返回这个删除的元素; * @return */ public E removeFirst() { return remove(0); } /** * 删除最后一个元素,并返回这个删除的元素; * @return */ public E removeLast() { return remove(size - 1); } /** * 判断数组中是否包含某个元素,如果有就删除;(其实,这儿删除的是第一个;比如数组中有三个位置都是77,如果我们删除77的话, * 删除的是第一个) * @param e */ public void removeElement(E e) { int index = find(e); if (index != -1) { remove(index); } } /** * 判断数组中是否包含某个元素,如果有就删除;同时,如果有,而且我们删除了的话,就返回true;如果没有就返回false; * @param e * @return */ public boolean removeElementIsOK(E e) { int index = find(e); if (index != -1) { remove(index); return true; } return false; } /** * 重写toString方法,打印数组的基本信息和元素内容; * @return */ @Override public String toString() { StringBuilder res = new StringBuilder(); res.append(String.format("Array: size = %d, capacity = %d\n", size, data.length)); res.append('['); for (int i = 0; i < size; i++) { res.append(data[i]); if (i != size - 1) { res.append(','); } } res.append(']'); return res.toString(); } /** * 工具方法:数组扩容或缩容; * @param newCapacity */ private void resize(int newCapacity) { E[] newDate = (E[]) new Object[newCapacity];//先创建一个新的、新容量的数组; for (int i = 0; i < size; i++) { newDate[i] = data[i];//把数组中的数据,全部复制到新的数组中去; } data = newDate;//然后,把数组变量data,指向这个新创建的数组; //说明两点:(1)newData是个方法内部属性,这个方法执行完之后,newData这个变量就会被回收; // (2)data这个变量原先指向的是原先那个数组的空间,现在data指向新的空间了,那么原先旧数组的空间也会被回收; } }
说明:
(1)添加元素的时候,如果数组空间不够了,我们就去扩容;
(2)删除元素的时候,如果数组闲置空间过多,我们就去缩小容;
(3)数组扩容或缩容的方法:resize()方法;
(4)上面的扩容和缩容的过程,对于用户来说是不可见的;
六:复杂度分析;
1.添加元素的操作:
(1)在数组后面增加一个元素,addLast(E e)方法:O(1):这个操作的时间复杂度,和数据的规模无关;
(2)在数组的第一个位置增加一个元素,addFirst(E e)方法:O(n):因为这个操作,需要把数组中的数据全部向后平移一位;
(3)在数组的指定位置增加一个元素,add(int index,E e):O(n/2)=O(n):这个具体复杂度是多少与index有关;
● 引入概率论的内容,认为index取0-size的可能性都是一样的;所以,可以得到其时间复杂度的期望;这儿就不详细介绍了;
因为,通常考虑时间复杂度时候,我们考虑最坏的情况;所以,对于添加元素来说,整体来看,其时间复杂度就是O(n);
(4)添加时,如果需要,还需要对数组进行扩容,resize(int newCapacity)方法:O(n);
2.删除元素的操作:
(1)删除数组后面的一个元素,removeLast()方法:O(1);
(2)删除数组的第一个元素,removeFirst()方法:O(n);
(3)删除指定位置的元素,remove(int index)方法:O(n/2)=O(n);
(4)删除时,如果需要,还需要对数组进行缩容,resize(int newCapacity)方法:O(n);
因为,通常考虑时间复杂度时候,我们考虑最坏的情况;所以,对于查询元素来说,整体来看,其时间复杂度就是O(n);
3.修改元素的操作:
(1)修改指定位置的元素,set(int index,E e):O(1);
因为,通常考虑时间复杂度时候,我们考虑最坏的情况;所以,对于修改元素来说,整体来看,如果已知索引其时间复杂是O(1),如果未知索引其时间复杂度就是O(n);
4.查询元素的操作;
(1)根据索引去查元素,get(int index):O(1);
(2)判断数组中是否包含某个元素,contains(E e):O(n);
(3)判断数组中是否包含某个元素,如果有,返回这个元素在数组中的索引,find(E e):O(n);
因为,通常考虑时间复杂度时候,我们考虑最坏的情况;所以,对于查询元素来说,整体来看,如果已知索引其时间复杂是O(1),如果未知索引其时间复杂度就是O(n);
七:引申:均摊时间复杂度,和,防止时间复杂度的震荡;
1.均摊时间复杂度;
(1)问题阐述:
● 逻辑阐述:【前面在添加操作的时间复杂度时,因为通常考虑时间复杂度时候,我们考虑最坏的情况;】→【但,在实际应用中,对于添加来说,我们可以只调用addLast( E e),尽量不调用addFirst(E e)和add(int index,E e);;那么此时,添加操作的时间复杂度就是O(1);】→【即使如此,因为在添加的时候,有时是需要扩容的,而介绍resize(int newCapacity)方法的时间复杂度是O(n);】→【索引,鉴于通常考虑时间复杂度时候,我们考虑最坏的情况】→【所以,即使是调用addLast(E e)方法,最坏的情况下,其时间复杂度依旧是O(n);】→【所以,我们认为添加操作时间复杂度,整体上看为O(n);】
● 但是,在实际上,我么调用addLast(E e)的时候,不可能每一次都会需要调用resize(int newCapacity)方法的;所以,对于addLast(E e)来说,依旧考虑最坏的、需要调用resize(int newCapacity)的情况,是稍微有些不合理的;
(2)均摊时间复杂度:
● 案例阐述:【假如我们创建了一数组,其capacity是8;然后,我们使用addLast(E e)向其中添加元素;】→【那么我们需要添加8次,直到第九次添加的时候,才会去调用resize(int newCapacity)方法;】→【那么,其实上面的过程,我们调用了9次addLast(E e),触发了一次resize(int newCapacity),总共进行了17次基本操作;平均来看,每次调用了addLast(E e),进行两次基本操作】;
● 案例阐述:一般化处理:【假如我们创建了一数组,其capacity是n;然后,我们使用addLast(E e)向其中添加元素;】→【那么我们需要添加n次,直到第n+1次添加的时候,才会去调用resize(int newCapacity)方法;】→【那么,其实上面的过程,我们调用了n+1次addLast(E e),触发了一次resize(int newCapacity),总共进行了2n+1次基本操作;平均来看,每次调用了addLast(E e),进行两次基本操作】;
● 那么,这样一看,均摊计算的话,addLast(E e)方法的时间复杂度是O(2),即O(1);
● 在这个案例中,均摊计算比计算最坏的情况,更加有意义;而,这种计算复杂度,就是均摊时间复杂度;在实际工程中,均摊时间复杂度还是有意义的;
● 自然,removeLast(E e)方法的均摊时间复杂度也是O(1);
2.时间复杂度的震荡;
(1)问题阐述:
● 【假如我们创建了一数组,其capacity是8;然后,此时数组已经满了】→【如果,此时我们使用addLast(E e)向其中添加元素;就会调用resize(int newCapacity)方法,那么此时addLast(E e)的时间复杂度就是O(n)】→【此时数组的capacity是16,然后size是9】→【然后,我们马上再去调用removeLast(E e),自然也会去触发resize(int newCapacity)方法,那么此时removeLast(E e)的时间复杂度就是O(n)】→【此时数组的capacity是8,然后size是8】→【然后,我们又去调用addLast(E e),……】→【然后,我们又去调用removeLast(E e),……】;
(2)问题原因分析:
● 调用removeLast(E e)的时候,我们过于着急的去调用resize(int newCapacity)方法,即采用了一种Eager的策略;
(3)解决方案:
● 调用removeLast(E e)的时候,可以采用一种lazy策略;比如:【假如我们创建了一数组,其capacity是8;然后,此时数组已经满了】→【如果,此时我们使用addLast(E e)向其中添加元素;就会调用resize(int newCapacity)方法,那么此时addLast(E e)的时间复杂度就是O(n)】→【此时数组的capacity是16,然后size是9】→【然后,我们马上再去调用removeLast(E e),此时数组的capacity是16,然后size是8】→【此时,我们不着急调用resize(int newCapacity),去缩容;】→【而是再等等,如果一直调用removeLast(E e),直到数组的capacity是16,size是4,即size是capacity是四分之一时,我们再来调用resize(int newCapacity),去缩容】→【此时,缩容依旧是缩为二分之一,即数组缩容后,capacity是8,然后size是4】;
● 这种lazy的策略,还是比较有意思的;在算法领域,有时我们懒一些,返回会使算法的整体性能会更好;;;后面介绍到线段树,也会介绍到类似的策略;
● removeLast(E e)采用lazy策略的实现: