一、动态数组的基本原理
实现一个基本的动态数组,需要实现一些基本的功能,增删改查应该是最基本的了,而在此基础之上还需要实现相应的动态扩容与泛型。对于动态扩容来说,我们需要在数组的使用过程中来进行判断,是否需要进行扩容,以及如何更有效的进行扩容,添加长度还是重新申请。而泛型的实现就需要数组能够接受任何类型的数据。
二、动态扩容
实现动态扩容的方式有很多,但是在数组的后面接上一段内存是明显不行的,我们需要做的就是重新申请一个更大容量的数组,这样才是可行的方法,但是问题又来了用什么方式来申请才能够最有效的扩容,我们不应该需要一个然后扩容一个(在扩容的时候我们还需要把旧数组中的数据转移到新数组中去)如果这样做的话会非常浪费性能和消耗时间的;所以我们选择扩容的时候让容量乘以一个特定的数,让其能够成倍数增长,这样我们就不需要频繁的去重新生成数组来扩容,当然也不会因为初始开辟的内存过大而浪费内存(这一点和java的实现方式是差不多),所以我们使用成倍数增长的方式来实现动态扩容。
三、泛型
泛型(E)也就是可以匹配任何类型,对于这一点java其实可以实现的很简单,原因就是java的所有类都会继承一个java.lang包中一个类就是Object类,所有的类都会默认继承这个Object类,因此我们只需要将泛型定义为Object类型就可以了。
但是泛型又会牵扯到一个java的自动装箱和自动拆箱的功能,Java为每种基本数据类型都提供了对应的包装器类型(int就是对应Integer,自动装箱:生成一个的10的integer对象只需要Integer i= 10,而不需要new Integer(10),反之拆箱就是直接把Integer对象的值赋值给int类型,而不需要类型装换),装箱:自动将基本数据类型转换为包装器类型;拆箱:就是自动将包装器类型转换为基本数据类型;而对于数组输入值进行查找的时候会有类型检索和拆装箱的操作所以会有一点性能损失,而这也造成了ArrayList是一个类型不安全的数组(当然是想对言的)
四、动态数组(顺序表)性质
顺序表(动态数组):将元素顺序地存放在一块连续的存储区里,元素间的顺序关系由它们的存储顺序自然表示,有两种情况:一体式结构、分离式结构
1、顺序表(动态数组)分类:一体式结构、分离式结构
-
一体式结构:直接存储数据
-
分离式结构:不直接存储数据,而是存储“数据的地址”
2、顺序表(动态数组)结构
顺序表(动态数组)的完整信息包括两部分:
- 数据区
- 信息区,即元素存储区的容量和当前表中已有的元素个数
3、顺序表(动态数组)扩充
数据区更换为存储空间更大的区域时,有两种策略:
-
每次扩充增加固定数目的存储位置,如每次扩充增加10个元素位置,这种策略可称为线性增长。 特点:节省空间,但是扩充操作频繁,操作次数多。
-
每次扩充容量加倍,如每次扩充增加一倍存储空间。 特点:减少了扩充操作的执行次数,但可能会浪费空间资源 , 以空间换时间,推荐的方式
4、顺序表(动态数组)元素存储区搬迁
顺序表(动态数组)存储在连续的空间,如果相邻的闲置空间被占用或不够用,则只能整体搬迁
5、顺序表(动态数组)增加元素
- 尾端加入元素,时间复杂度为O(1)
- 非保序的加入元素(不常见),时间复杂度为O(1)
- 保序的元素加入,时间复杂度为O(n)
6、顺序表(动态数组)删除元素
- 删除表尾元素,时间复杂度为O(1)
- 非保序的元素删除(不常见),时间复杂度为O(1)
- 保序的元素删除,时间复杂度为O(n)
7、Python中的顺序表(动态数组):list、tuple
Python中的list和tuple两种类型采用了顺序表(动态数组)的实现技术,具有前面讨论的顺序表(动态数组)的所有性质。tuple是不可变类型,即不变的顺序表(动态数组),因此不支持改变其内部状态的任何操作,而其他方面,则与list的性质类似。
Python标准类型list就是一种元素个数可变的线性表,可以加入和删除元素,并在各种操作中维持已有元素的顺序(即保序),而且还具有以下行为特征:
-
基于下标(位置)的高效元素访问和更新,时间复杂度应该是O(1);为满足该特征,应该采用顺序表(动态数组)技术,表中元素保存在一块连续的存储区中。
-
允许任意加入元素,而且在不断加入元素的过程中,表对象的标识(函数id得到的值)不变。为满足该特征,就必须能更换元素存储区,并且为保证更换存储区时list对象的标识id不变,只能采用分离式实现技术。
-
在Python的官方实现中,list就是一种采用分离式技术实现的动态顺序表(动态数组)。这就是为什么用list.append(x) (或 list.insert(len(list), x),即尾部插入)比在指定位置插入元素效率高的原因。
-
在Python的官方实现中,list实现采用了如下的策略:在建立空表(或者很小的表)时,系统分配一块能容纳8个元素的存储区;在执行插入操作(insert或append)时,如果元素存储区满就换一块4倍大的存储区。但如果此时的表已经很大(目前的阀值为50000),则改变策略,采用加一倍的方法。引入这种改变策略的方式,是为了避免出现过多空闲的存储位置。
8、顺序表(动态数组)的不足
- 内存充足时,增加元素时,元素存储区整体搬迁没问题,但是如果内存不足,则没办法进行元素存储区的整体搬迁
五、动态数组的基本结构
模拟动态数组的接口设计实现
- int size() // 数组元素的个数
- boolean isEmpty() // 判断数组是否为空
- boolean contain(E e) // 判断数组中是否含有该数据项
- E get(int index) // 返回对应位置的数组元素
- E set(int index, E e)// 设置相应索引位置的元素的值,并且返回原来的元素的值
- void add(E e) // 往数组的末尾添加元素,超长则扩容
- void add(int index, E e)// 往相应的索引位置添加于元素,后面的数组元素后移一位
- E remove(int index) // 删除指定位置的元素,后续数据全部前移一位
- int indexOf(E e) // 获取某个元素在数组中的位置
- void clear() // 清空数组
- void check() //检查数组索引是否越界
- void capacity() // 判断是否需要扩容
1、初始化数组ArrayList
初始化数组可以把一些不需要改变的量设置为常量,比如初始数组容量,并设定好相应的全局变量。初始化数组函数一般会提供两个,一个是空参构造器一个是含参构造器(含参构造器可以由用户决定初始化数组容量的大小)。
public class ArrayList<E> { // 这里E代表泛型
private int size = 0; // 我们初始化定义数组的内部元素个数为0
private E[] elements; // 声明一个数组变量
private static final int NUM = 5; // 我们把常量定义为静态变量,这样他就只会占用一次内存
/**
* 空参构造器,默认定义数组的容量为10个元素
*/
public ArrayList() {
// elements = new E[num]; //可以直接定义,也可以调用含参构造器来实现
this(NUM);
}
/**
* 含参构造器,通过num参数传递数组容量
* @param num
*/
public ArrayList(int num) {
// 这里需要对传入的参数进行判断,防止出现负数的情况
num = (num < NUM) ? NUM : num;
elements = (E[]) new Object[num];
}
2、返回数组元素个数Size()与判空
数组元素个数初始为0,随后的更新数组操作都会同步的更新size变量的值,所以直接返回size变量即可。判空的话就直接判断size是否为0就行。
/**
* 获取数组的元素个数
* @return
*/
public int size() {
return size;
}
/**
* 判断动态数组是否为空
* @return
*/
public boolean isEmpty() {
// 如果size大于0返回false非空,反之返回true为空
return size == 0;
}
3、索引越界异常函数Check()
判断索引是否正常的函数,而这个函数又有两种情况,一个是操作元素还未满的情况(例如删除元素)我们只需要检查0<索引>=size-1的索引即可;但是当添加元素的时候,是允许添加到size(末尾的后一个元素)索引位的,这时候就需要0<索引>size即可,因为需要扩容操作才可以实现后续添加。
/**
* 由于错误抛出代码复用过多,进行封装,一般内部使用的方法设置为私有的
*/
private void checkMessage() {
throw new IndexOutOfBoundsException("索引越界");
}
// 对于删除情况使用
private void Check(int index) {
if (index < 0 || index >= (size - 1)) {
checkMessage();
}
}
// 对于添加情况使用
private void checkAdd(int index) {
if (index < 0 || index > size) {
checkMessage();
}
}
4、获取Get()与修改Set()函数
获取数组元素,只要索引判断正确即可。修改数组元素并且返回旧元素值也是同上一样的条件即可。
/**
* 获取数组的相应位置的元素
* @param index
* @return
*/
public E get(int index) {
Check(index);
return elements[index];
}
/**
* 在数组的指定位置修改元素,并将旧元素返回
* @param index
* @return
*/
public E set(int index, E e) {
Check(index);
E old = elements[index];
elements[index] = e;
return old;
}
5、删除元素remove()函数
删除某个位置的元素只需要,将后面的元素前移覆盖掉需要删除位置的元素,并且把最后一元素的指向置NULL即可。
/**
* 删除指定位置的元素
* @param index
* @return
*/
public E remove(int index) {
Check(index);
E before = elements[index];
// 删除指定位置的元素,只需要把后面的元素前移即可,不用挖去该位置的元素
for (int i=index+1; i<size; i++) {
// 把后一个赋值给前一个
elements[i-1] = elements[i];
}
size--;
elements[size] = null; // 把最后一个元素的指向地址清空
return before;
}
6、获取元素在数组中的位置indexOf()
有两种状况需要判断,一种是传入非空元素比较到数组中的null值元素;第二种就是传入null值,就直接找到数组中第一个null元素的位置返回,否者二者都是返回-1没有找到相应元素(这里null对象是不可以调用equals()方法的,会出现空指针异常,所以会调换调用顺序)。
/**
* 获取指定元素在数组中的位置
* @param e
* @return
*/
public int indexOf(E e) {
if (e == null) {
for (int i=0; i<size; i++) {
if (elements[i] == null) return i; //直接返回第一个null元素
}
}else {
for (int i=0; i<size; i++) {
// if (elements[i] == e) return i; // 等号是比较两个的地址,equals()方法可以自定义重写
if (e.equals(elements[i])) return i; // e不为null,用e来调用方法,防止出现空指针异常
}
}
return -1;
}
7、扩容函数Capacity()
数组的动态扩容就是使用前面的说的方式进行扩容,同过数组容量的增加相应的倍数实现
/**
* 数组扩容函数
* @param capacity
*/
private void Capacity(int capacity) {
// 先获取旧容量,判断需要扩容则扩容,反之什么都不做
int oldCapacity = elements.length;
if (oldCapacity >= capacity) return;
// 反之进行扩容,创建一个更大的新数组,把数据挪过去就可以了,内存的话有垃圾回收机制
// 这里新容量一般是原容量乘以某个数,不用一个一个加容量设置为原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1); // 位运算符,左移一位除以2,在java中是以二进制补码进行运算
E[] newElements = (E[]) new Object[newCapacity];
for (int i=0; i<size; i++) {
newElements[i] = elements[i];
}
elements = newElements;
}
8、添加函数add()
添加有默认末尾添加(末尾是调用中间插入,只需要把size当成参数传入即可),还有中间插入添加,需要把元素后移完成,空出相应位置的元素才可以进行添加操作,而上面两者都需要判断是否需要扩容才可以添加元素。
/**
* 往数组的末尾添加元素
* @param e
*/
public void add(E e) {
// 小于NUM的情况,需要添加的元素的索引就是size的值(当size为5则有5个元素,实际添加的索引位置就是5)
// elements[size++] = e;
// size++
add(size,e); // 这里可以直接调用add方法,传入size作为索引即可
}
/**
* 往数组的指定位置添加元素,支持存入null元素
* @param index
* @param e
*/
public void add(int index, E e){
// 先判断是否需要扩容
Capacity(size+1);
// 这里我们也需要对index进行一下限制,防止出现索引越界
checkAdd(index);
// 插入元素的话需要把原来的元素后移(从后往前移),然后再插入
for (int i=size-1; i>=index; i--) {
elements[i+1] = elements[i];
}
elements[index] = e;
size++;
}
9、清空数组clear()和重写toString()方法
清空数组需要把每一个元素的指向都置为null,但是不用释放数组的内存;而toString()方法是改变输出格式,否则就是输出地址。
/**
* 清空该数组
*/
public void clear() {
for (int i=0; i<size; i++) {
elements[i] = null; // 我们只需要把数组元素指向对象地址的线断开即可(没有指向引用会被垃圾回收),数组需要留下
}
size = 0;
// 浪费空间问题,每次使用都向堆空间申请和销毁才浪费性能。
}
/**
* 重写toString()方法
*/
public String toString() {
// 想要数组的输出结果是[1,2,3]
StringBuilder string = new StringBuilder();
string.append("size="+size+"[");
// 中间是需要拼接的数据
for (int i=0; i<size; i++) {
string.append(elements[i]);
if (i < size-1) string.append(",");
}
string.append("]");
return string.toString();
}
清空数组的实际操作:实际我们数组对象只是指向了一个存储这对象的内存地址的一个数组,真正的对象其实是任意的储存在内存中的,而java的垃圾回收机制是没人用(没有变量指向)就会被自动回收,所以我们只需要把数组中存储的对象的地址置空就可以了,而不需要把数组list的指向也切断。
五、动态数组完整代码
MyArray.java
public class MyArray<T> {
private T[] arr;
private int size;
public MyArray(int capacity){
arr = (T[])new Object[capacity];
size = 0;
}
//默认构造方法,初始化最大容量为10
public MyArray(){
this(10);
}
//获取数组的大小
public int getSize(){
return size;
}
//获取数组最大容量
public int getCapacity(){
return arr.length;
}
public boolean isEmpty(){
return size == 0;
}
//向数组末尾添加元素
public void add(T elem){
add(size, elem);
}
//向数组中指定位置添加元素
public void add(int index, T elem){
if (index < 0 || index > size){
throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
}
if (size == arr.length){
resize(arr.length * 2);
}
for (int i = size - 1; i >= index; i--){
arr[i + 1] = arr[i];
}
arr[index] = elem;
size++;
}
//获取指定位置的元素
public T get(int index){
if (index < 0 || index >= size){
throw new IllegalArgumentException("Get faild. Index is illegal.");
}
return arr[index];
}
//修改指定位置的元素
public void set(int index, T elem){
if (index < 0 || index >= size){
throw new IllegalArgumentException("Set faild. Index is illegal.");
}
arr[index] = elem;
}
//查找数组中是否包含该元素
public boolean contains(T elem){
for (int i = 0; i < size; i++){
if (arr[i].equals(elem)){
return true;
}
}
return false;
}
//查找该元素的下标
public int indexOf(T elem){
for (int i = 0; i < size; i++){
if (arr[i].equals(elem)){
return i;
}
}
return -1;
}
//删除数组末尾的元素
public T remove(){
return remove(size - 1);
}
//删除数组中指定位置的元素
public T remove(int index){
if (index < 0 || index >= size){
throw new IllegalArgumentException("Remove faild. Index is illegal.");
}
T elem = arr[index];
for (int i = index + 1; i < size; i++){
arr[i - 1] = arr[i];
}
size--;
arr[size] = null;
if (size == arr.length / 4 && arr.length / 2 != 0){
resize(arr.length / 2);
}
return elem;
}
//删除数组中指定的元素
public boolean remove(T elem){
int index = indexOf(elem);
if (index != -1){
remove(index);
return true;
}
return false;
}
//对数组进行扩容或缩容
private void resize(int capacity){
T[] newarr = (T[])new Object[capacity];
for (int i = 0; i < size; i++){
newarr[i] = arr[i];
}
arr = newarr;
}
@Override
public String toString() {
StringBuilder string = new StringBuilder();
string.append("[");
for (int i = 0; i < size; i++){
string.append(arr[i]);
if (i != size - 1){
string.append(", ");
}
}
string.append("]");
return string.toString();
}
}
Main.java
public class Main {
public static void main(String[] args) {
//测试一下写的这几个方法
MyArray<Integer> myArray = new MyArray<>();
System.out.println("myArray.getCapacity() = " + myArray.getCapacity());
System.out.println("myArray.getSize() = " + myArray.getSize());
System.out.println("myArray.isEmpty() = " + myArray.isEmpty());
System.out.println("myArray = " + myArray);
System.out.println("========================================================================================");
myArray.add(1);
System.out.println("myArray.getCapacity() = " + myArray.getCapacity());
System.out.println("myArray.getSize() = " + myArray.getSize());
System.out.println("myArray.isEmpty() = " + myArray.isEmpty());
System.out.println("myArray = " + myArray);
System.out.println("========================================================================================");
myArray.add(2);
System.out.println("myArray.getCapacity() = " + myArray.getCapacity());
System.out.println("myArray.getSize() = " + myArray.getSize());
System.out.println("myArray.isEmpty() = " + myArray.isEmpty());
System.out.println("myArray = " + myArray);
System.out.println("========================================================================================");
myArray.add(0, 0);
System.out.println("myArray.getCapacity() = " + myArray.getCapacity());
System.out.println("myArray.getSize() = " + myArray.getSize());
System.out.println("myArray.isEmpty() = " + myArray.isEmpty());
System.out.println("myArray = " + myArray);
System.out.println("========================================================================================");
myArray.set(0, -1);
System.out.println("myArray.get(0) = " + myArray.get(0));
System.out.println("myArray.getCapacity() = " + myArray.getCapacity());
System.out.println("myArray.getSize() = " + myArray.getSize());
System.out.println("myArray.isEmpty() = " + myArray.isEmpty());
System.out.println("myArray = " + myArray);
System.out.println("========================================================================================");
myArray.remove(0);
myArray.add(0, 0);
System.out.println("myArray.getCapacity() = " + myArray.getCapacity());
System.out.println("myArray.getSize() = " + myArray.getSize());
System.out.println("myArray.isEmpty() = " + myArray.isEmpty());
System.out.println("myArray = " + myArray);
}
}
输出结果:
myArray.getCapacity() = 10
myArray.getSize() = 0
myArray.isEmpty() = true
myArray = []
========================================================================================
myArray.getCapacity() = 10
myArray.getSize() = 1
myArray.isEmpty() = false
myArray = [1]
========================================================================================
myArray.getCapacity() = 10
myArray.getSize() = 2
myArray.isEmpty() = false
myArray = [1, 2]
========================================================================================
myArray.getCapacity() = 10
myArray.getSize() = 3
myArray.isEmpty() = false
myArray = [0, 1, 2]
========================================================================================
myArray.get(0) = -1
myArray.getCapacity() = 10
myArray.getSize() = 3
myArray.isEmpty() = false
myArray = [-1, 1, 2]
========================================================================================
myArray.getCapacity() = 5
myArray.getSize() = 3
myArray.isEmpty() = false
myArray = [0, 1, 2]
Process finished with exit code 0