这篇笔记开始 将系统学习一遍数据结构,编程的技术内功。💪💪💪
我们先看下在Java中如何使用数组
1 | public class Main { |
二次封装动态数组
数组最大的优点:快速查询。scores[2]
数组最好应用于”索引有语义”的情况。(即索引下标最好有实际意义)
但是并非所有有语义的索引都适用于数组。(如身份证号码)
下面我们主要探索索引没有语义的情况数组的使用。
索引没有语义,如何表示没有的元素?
如何添加元素,删除元素?
我们自己设计一个数组,能够实现索引没有语义的情况解决上面的问题
1 | public class Array { |
下面我们逐渐完善这个类。
向数组中添加元素
上面代码写了这个size
就是数组data
的实际有效元素个数,指向的是数组中第一个没有元素的位置。我们要向数组末尾添加元素,直接向size
指向的位置添加元素。同时size
要向后挪移位。
1 | // 向所有元素后添加一个新元素 |
向指定位置添加元素
对于一个有序的数组,我们可以指定插入数据的位置。
在指定位置添加新的元素,我们需要将指定位置后面的元素都往后挪一位,然后将要添加的元素放到对应的位置。
1 | // 在第index个位置插入一个新元素e |
了上面的方法我们可以简化添加最后和最开始两个函数
1 | // 向所有元素后添加一个新元素 |
在数组中查询元素和修改元素
我们重写一下toString
方法
1 |
|
新增一个数组,打印输出下
1 | Array arr = new Array(20); |
1 | // 在 1 位置添加 100 |
1 | // 开始位置添加 -1 |
1 | // 获取 index 索引位置的元素 |
增加一个get
方法使用户只能访问有数据的空间。
1 | // 修改 index 索引位置的元素为e |
增加set
方法设置指定索引的值。
增加包含,搜索,删除方法
遍历查找元素
1 | // 查找数组中是否有元素 |
遍历查找元素,返回对应的索引
1 | // 查找数组中元素 e 所在的索引 如果不存在元素 e 则返回 -1 |
删除指定位置的元素
类比于插入元素,我们在删除元素的时候,需要将对应位置后面的元素都往前挪一位。最后需要将size
加一。不过,因为我们将size
指向的是第一个为空的位置,最后一个元素我们可以不删,因为我们将它看为空。
1 | // 从数组中删除index位置的元素 返回删除的元素 |
由上面方法我们可以创造两个简化的方法
1 | // 从数组中删除第一个元素 返回删除的元素 |
删除指定元素
1 | // 删除数组中元素 e |
使用泛型
我们上面的数组只能承载int
类型的元素。一个数组应该能够存放任意类型的数据。
解决这个问题的方式就是使用泛型。
使用泛型可以让我们的数据结构放置”任何”(不可以是基本数据类型,只能是类对象)数据类型。
基本数据类型(boolean
byte
char
short
int
long
float
double
)
每个基本数据类型都有对应的包装类。因此使用泛型的时候,我们存放的是基本类型的包装类。
基本数据类型对应的包装类(Boolean
Byte
Char
Short
Int
Long
Float
Double
)
我们使用泛型修改上面写的数组类,使其能够承载任意类型数据。
1 | public class Array<E> { |
我们在使用Array
的时候只要指定泛型类型即可
1 | public class Main { |
1 | public class Main { |
我们还可以使用自己定义的类型
1 | public class Student { |
动态数组
上面我们使用的数组data
还是Java内置的静态数组,具有一些局限性(容量有限)。
当我们的数组已满 继续想添加元素的时候,可以先新建一个容量长一点的数组,然后将旧的数组元素拷贝到新的数组中,并将之前的data
指向新的数组。旧的数组会被垃圾回收机制自动回收。这样我们就得到了一个新的数组。
1 | // 将数组的容量扩容到之前的两倍 |
1 | // 在index索引的位置插入一个新元素e |
经过上面的改造 我们的数组就是动态数组了
我们测试下 当数组满了的时候 再次新增元素将会发生什么
1 | public class Main { |
我们看到在数组满的时候,将会扩容然后新增数据。
同理 我们可以在删除数组的时候当存储数据只有容量的一半的时候,可以将容量减少。
1 | // 从数组中删除index位置的元素, 返回删除的元素 |
测试一下当数据量为容量一半的时候
1 | public class Main { |
我们看到 当数据量减少到容量一半的时候 将会自动缩减容量。
简单的时间复杂度分析
我们在讨论时间复杂度的时候经常遇见O(1)
, O(n)
, O(lgn)
, O(nlogn)
, O(n^2)
。
大O描述的是算法的运行时间和输入数据之间的关系。
不过这个大O表示的是渐进时间复杂度,描述n趋近于无穷的情况。
现在我们看下之前写的Array
各个函数的时间复杂度
当索引具有语义的时候,时间复杂度就会很小。
均摊复杂度
我们以上考虑都是基于最坏考虑的,但是在新增的时候只有新增到最后需要扩容的时候resize
才会有O(n)
。
这种最坏的考虑是不适合addLast
的时间复杂度计算的。
当我们使用均摊复杂度(amortized time complexity)的时候 addLast
的时间复杂度则为O(1)
因为扩容的触发是在2n+1
次addLast
之后才触发一次。
复杂度震荡
在刚好到容量长度的时候每次新增删除都是触发O(n)
。理论上这两个方法的时间复杂度都是O(1)
。
出现问题原因:removeLast
时resize
过于着急(Eager)
解决方案:Lazy的方式resize
即当使用removeLast
删除元素的时候,并不着急的缩容,而是继续可以进行数据删除,当数据量为扩容后的1/4
(即原来容量的1/2
)的时候再进行缩容。
这样我们可以修改下我们的代码
1 | // 从数组中删除index位置的元素, 返回删除的元素 |