文章目录
核心目标
- 通过手动实现简化版
ArrayList
和LinkedList
,深入理解Java集合框架底层原理。 - 对比两种数据结构的性能差异及适用场景。
自定义ArrayList实现
1.底层结构与初始化
- 基于动态数组实现,核心成员变量:
对象数组 object[] obj
数组存储数据的个数 int size
private static final int DEFAULT_CAPACITY = 10;//初始容量
private Object[] obj;
private int size;
- 构造方法设计(默认容量、指定容量)。
默认容量
public MArrayList() {
obj = new Object[DEFAULT_CAPACITY];
size = 0;
}
指定容量
public MArrayList(int capacity) {
if (capacity > 0) {
obj = new Object[capacity];
size = 0;
}
}
2.关键方法实现
动态扩容机制
- 触发条件:插入元素时检查
size == obj.length
。 - 扩容策略:新数组大小为原容量的1.5倍(模拟JDK实现)。
- 思路:创建一个 数组长度为原数组1.5倍的 新数组,复制obj从0~size的数据 到 newObj 的0~size位置
//扩容
public void resize() {
int newSize = (int) (obj.length * 1.5);
Object[] newObj = new Object[newSize];
//更新数组,复制obj从0-size的数据 到 newObj的0-size位置
if (size >= 0)
System.arraycopy(obj, 0, newObj, 0, size);
obj = newObj;
}
增删改查操作
add(E element)
:尾插法。
1.判断是否需要扩容。
2.将新元素直接添加在数组末尾,size++。
//添加到末尾
public void add(E data) {
//对原数组扩容
if (size == obj.length) {
resize();
}
obj[size] = data;
size++;
}
add(int index, E element)
:索引检查、数据搬移(System.arraycopy
)。
判断是否需要扩容。
1.创建一个 数组长度为 原数组长度 的新数组,size++。
2.将index前的数据复制到对应位置:复制obj从0~index的数据 到 newObj 的0~index位置,
3.将data插入数组的index位置。
4.将剩余元素后移一位,需要复制的数据个数为 size-index。
//插入到指定位置
public void insert(int index, E data) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException();
if (size == obj.length) {
resize();
}
Object[] newObj = new Object[obj.length];
size++;
//index前数据复制到对应位置
System.arraycopy(obj, 0, newObj, 0, index);
Object temp = obj[index];
newObj[index] = data;
//后移一位,需要复制的数据个数为 size-index
if (size - index >= 0)
System.arraycopy(obj, index, newObj, index + 1, size - index);
newObj[index + 1] = temp;
obj = newObj;
}
remove(int index)
:边界校验、元素左移。
1.检验index是否超出边界
2.如果index是obj的最后一位(index从0开始,所以最后一位的下标为size-1),直接将最后一个元素赋为null,size- -,返回index对应位置存的数据。
//移除指定位置数据
public E remove(int index) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException();
if(index == size-1) {
Object temp = obj[index];
obj[--size] = null;
return (E) temp;
}
Object temp = obj[index];
for (int i = index; i < size; i++) {
obj[i] = obj[i + 1];
}
obj[size] = null;
size--;
return (E) temp;
}
removeFirst(E element)
:删除指定数据,如果有多个相同数据,按数组下标的顺序删除第一个找到的
1.查找数组中是否有data元素,如果找到了保存下标
2.从index开始,将后面数组前移
//移除指定数据
public boolean removeFirst(int data) {
int index = -1;
for (int i = 0; i < size; i++) {
if (obj[i].equals(data)) {
index = i;
}
}
if (index == -1)
return false;
for (int i = index; i < size; i++) {
obj[i] = obj[i + 1];
}
obj[size] = null;
size--;
return true;
}
get(int index)
:时间复杂度O(1)的直接访问。
//获取
public E get(int index) {
if (index < 0 || index >= size)
return null;
return (E) obj[index];
}
泛型
本文在实现ArraList时使用了泛型,允许不同数据类型使用该类,但是插入ArrayList的这组元素必须都为声明时的类型。
public class MArrayList<E> {
}
例如,
MArrayList<Integer> list = new MArrayList<>();
此时插入的元素必须都为Integer类型。
自定义LinkedList实现
1.节点结构与初始化
- 节点类定义:(本文使用单向链表实现LinkedList)
class Node {
public Object data;
public Node next;
public Node(Object data) {
this.data = data;
}
public Node() {
}
}
-
核心成员变量
private Node root;//根节点,root.next为头结点
public Node last;
public int size; -
构造方法
public LinkList() {
root = new Node();
size = 0;
}
2.关键方法实现
插入与删除
add(int index, E element)
:尾插法
1.如果还没有插入节点,则此结点为头节点。
2.如果已插入节点,在last后面插入新节点。
3.size++
public void add(E data) {
Node node = new Node(data);
Node head = root.next;
if (head == null) {
root.next = node; //头节点
last = node;
} else {
last.next = node;
last = node;
}
size++;
}
insert(int index,E data)
在指定位置插入元素
1.如果index=0,则插入的是头结点。
2.如果index不为0,先从头遍历至index的前一个节点,重新在index处建立关联,如果index=size,则此节点为尾节点。
3.size++
- public void insert(int index, E data) {
if (index < 0 || index > size) {
System.out.println("Index out of bounds");
return;
}
Node newNode = new Node(data);
if (index == 0) {
newNode.next = root.next;
root.next = newNode;
if (size == 0)
last = newNode;
} else {
Node prev = root;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
newNode.next = prev.next;
prev.next = newNode;
if (index == size)
last = newNode;
}
size++;
}
remove(int index)
:解除节点关联、处理头尾边界。逻辑和insert类似。
1.如果index=0,去除头结点root.next,重新建立关联。
2.从头遍历值index的前一个节点,保存index节点数据,重新建立与后继结点的关联。
3.size- -,返回remove的数据。
public E remove(int index) {
Node pre = root.next;
if (index == 0) {
root.next=pre.next;
return (E)pre.data;
// if (size == 0)
// System.out.println("List is empty");
}
else if (index < 0 || index > size) {
System.out.println("Index out of bounds");
return null;
}
for (int i = 0; i < index-1; i++) {
pre = pre.next;
}
Node remove = pre.next;
pre.next = remove.next;
if (index == size - 1)
last = pre;
size--;
return (E)remove.data;
}
遍历性能分析
get(int index)
:根据索引位置选择从头遍历(时间复杂度O(n/2))。
public Object get(int index) {
if (index < 0 || index >= size) {
System.out.println("Index out of bounds");
return -1;
}
Node cur = root.next;
for (int i = 0; i < index; i++) {
cur = cur.next;
}
return cur.data;
}
3. 其他方法
reverse()
实现链表的翻转。
public void reverse() {
Node pre = null;
Node cur = root.next;
Node tmp = null;
while (cur != null) {//cur不为尾节点
tmp = cur.next;//cur与cur.next交换位置
cur.next = pre;
pre = cur;
cur = tmp;
}
root.next = pre;
}
display()
展示链表内容,便于直观地看到修改后的效果。
public void display() {
Node cur = root.next;
int index = 1;
for (int i = 0; i < size; i++) {
if (cur != null) {
System.out.print( cur.data + " ");
cur = cur.next;
index++;
}
}
System.out.println();
}
性能对比与应用场景
1.时间复杂度对比
操作 | ArrayList | LinkedList |
---|---|---|
随机访问 | O(1) | O(n) |
头部插入 | O(n) | O(1) |
中间删除 | O(n) | O(n) |
2. 适用场景建议
ArrayList
:读多写少、需要频繁随机访问。LinkedList
:频繁在头部/尾部插入删除、无需随机访问。
总结
自定义实现Arraylist·和Linkedlist的意义:通过实现这两个基础集合类,开发者能够更直观地理解底层数据结构,并且能够根据需求优化特定场景的性能。
若发现错误之处,恳请读者批评指正,我将及时修正完善。