从0开始手写ArrayList动态数组和LinkedList双向链表
Posted 小胖java攻城狮
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从0开始手写ArrayList动态数组和LinkedList双向链表相关的知识,希望对你有一定的参考价值。
源代码码云Gitee下载链接
ArrayList动态数组
1.什么是数组?
在了解什么是数组之前,我们先了解什么是线性表,线性表是具有 n 个相同类型元素的有限序列,数组就是一种线性的数据结构,是一种顺序存储的线性表,所有元素的内存地址是连续的
2.为什么数组查询效率这么快?
处理速度由快到慢排序:
1、CPU寄存器 2、CPUL1缓存 3、CPUL2缓存 4、内存 5、硬盘
因为CPU缓存会读入一段连续的内存,顺序存储符合连续的内存,所以顺序存储可以被缓存处理,而链接存储并不是连续的,分散在堆中,所以只能内存去处理。
第一:Java的数组中存储的每个元素类型一致,也就是说每个元素占用的空间大小相同。
第二:Java的数组中存储的每个元素在空间存储上,内存地址是连续状态的。
第三:通常首元素的内存地址作为整个数组对象的内存地址,可见我们是知道首元素内存地址的。
第四:再加上数组中的元素是有下标的,有下标就可以计算出被查找的元素和首元素的偏移量。
综上所述,实际上在数组中查找元素是可以通过数学表达式计算被查找元素的内存地址的,通过内存地址可以直接定位该元素。也就是说数组中有100个元素和有100万个元素,实际上在查找方面效率是一样的。
3.什么是动态数组?
由于数组有一个缺点就是大小是固定的,这时候我们需要引入可以对数组容量扩容和缩容的操作,所以这类数组称之为动态数组。
4.动态数组存基本类型和对象(引用)类型有什么区别?
-
基本类型动态数组存储的是数据
-
对象类型动态数组存储的是对象的内存地址
以下是动态数组存储对象类型的示例图
5.接口设计
- int size(); // 元素的数量
- boolean isEmpty(); // 是否为空
- boolean contains(E element); // 是否包含某个元素
- void add(E element); // 添加元素到最后面
- E get(int index); // 返回index位置对应的元素
- E set(int index, E element); // 设置index位置的元素
- void add(int index, E element); // 往index位置添加元素
- E remove(int index); // 删除index位置对应的元素
- int indexOf(E element); // 查看元素的位置
- void clear(); // 清除所有元素
6.代码实现重点和难点讲解
添加 add(int index, E element)
/**
* 在index位置插入一个元素
*/
public void add(int index, E element)
//检查下标正确性
rangeCheckForAdd(index);
//是否需要扩容
ensureCapacity(size + 1);
//从左往右挪动
for (int i = size; i > index; i--)
elements[i] = elements[i - 1];
elements[index] = element;
size++;
这个从左往右挪动是什么意思?
假设存在数组array。
假设我们要想在 3 位置添加元素 88 ,那么按照我的代码逻辑,从左往右挪动的意思是,从66开始移动,左边的下标为 5 的 66 移动到右边的下标为 6 的位置,以此类推。
绿色是移动的顺序,那么紫色部分就是待添加元素 88 添加的位置。
大功告成!!!
删除 E remove(int index)
/**
* 删除index位置的元素
* @return 被删除的元素
*/
public E remove(int index)
//检查下标正确性
rangeCheck(index);
E old = elements[index];
//从右往左挪动
for (int i = index + 1; i < size; i++)
elements[i - 1] = elements[i];
elements[--size] = null;
//是否需要缩容
trim();
return old;
假设存在数组array
假设我们想删除下标为 2 的元素 33 ,从右往左挪动的意思是,从44开始移动,右边的下标为 3 的 44 移动到左边的下标为 2 的位置,以此类推。
红色是移动的顺序,那么黑色部分就是待元素变为 null 位置。
大功告成!!!
扩容操作
我们先来看一下JDK官方自带的ArrayList是怎么扩容的
由此我们可以得知,JDK是使用了Arrays.copyOf方法进行元素的扩容拷贝,而这个方法又调用了arraycopy方法,有native关键字修饰,说明是调用了JVM的本地库接口,是一个系统级别的操作,效率高。
但是我为了了解拷贝的思想,就没有调用这个API,而是自己实现了数组的拷贝。
如果所需的容量大于当前容量,就要进行扩容操作
- 先将旧数组容量扩容为原来的1.5倍,比如以前容量是10,扩容后就变成15
- 创建一个新数组保存新的容量
- 然后遍历原来的旧数组,将旧数组的元素全部复制到新数组
- 将新数组的引用地址覆盖旧数组的引用地址,这部的操作是为了统一上面的代码
/**
* 保证要有capacity的容量
* @param capacity
*/
private void ensureCapacity(int capacity)
int oldCapacity = elements.length;
if (oldCapacity >= capacity) return;
// 新容量为旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++)
newElements[i] = elements[i];
elements = newElements;
那这个扩容的方法什么时候会执行呢?
在存放元素的时候就会进行判断。
缩容操作
为了节省空间
private void trim()
// 30
int oldCapacity = elements.length;
// 15
int newCapacity = oldCapacity >> 1;
if (size > (newCapacity) || oldCapacity <= DEFAULT_CAPACITY) return;
// 剩余空间还很多
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++)
newElements[i] = elements[i];
elements = newElements;
LinkedList双向链表
1.什么是链表?
动态数组有个明显的缺点,可能会造成内存空间的大量浪费,能否用到多少就申请多少内存?
链表可以办到这一点
链表是一种链式存储的线性表,所有元素的内存地址不一定是连续的
2.单向链表和双向链表是什么?
单向链表
双向链表
3.接口设计
链表和动态数组差不多,关键是 Node节点类的设计,我这里采用的是内部类的形式
private static class Node<E>
E element; //元素
Node<E> prev; //前一个节点
Node<E> next; //后一个节点
public Node(Node<E> prev, E element, Node<E> next)
this.prev = prev;
this.element = element;
this.next = next;
双向链表VS动态数组,性能对比,何时选择其一
动态数组:开辟、销毁内存空间的次数相对较少,但可能造成内存空间的浪费(可以通过缩容解决)
双向链表:开辟、销毁内存空间的次数相对较多,但不会造成内存空间的浪费。
如果频繁在尾部进行添加、删除的操作,动态数组、双向链表均可选择。
如果频繁在头部进行添加、删除操作,建议使用双向链表。
如果有频繁的(在任意位置)进行添加、删除操作,建议使用双向链表。
如果频繁进行查询操作(随机访问操作),建议使用动态数组。
以上是关于从0开始手写ArrayList动态数组和LinkedList双向链表的主要内容,如果未能解决你的问题,请参考以下文章