数组链表合体 - 链式数组结构!集合链表和数组的优点于一身,一百万数据的随机添加以及随机删除最快只需 2.2 秒,是 ArrayList 的 70 倍 !
简介及测试结果
我们都知道,java 中最常用的集合是 ArrayList、LinkedList,前者是数组实现的,后者是双向链表实现的。
对于 ArrayList 来说适合读多写少的场景,因为它写数据的时候需要扩容或者移动数据;
对于 LinkedList 来说则适合写多读少的场景,因为它读数据的时候需要对节点进行遍历。
那么问题来了,有没有一种数据结构能够综合这两种数据结构的优点呢? 于是我设想了这样一种数据结构,我称它为链式数组结构: 那么基于这种结构,它的操作如下:
插入元素:首先确定 index 应该在哪个数组的范围。然后判断当前数组是否已经达到最大容量。 如果没有,直接按照 ArrayList 的方式插入即可; 如果达到了,那么判断 next 指针是否为空。 如果 next 为空,则创建一个新的数组,并让 next 指向这个新数组; 如果 next 不为空,那么判断 next 的数组是否达到最大容量。 如果没有达到最大容量,那么将所有的数据向后移动一位; 如果达到了最大容量,那么在当前数组和 next 数组的中间插入一个新的数组。 最后新数组的第一位数据赋值为当前数组的最后一位数据。 接着当前数组的数据向后移动一位,将元素插入当前位置。
删除操作:首先确定 index 应该在哪个数组的范围,然后直接根据 index 删除数据,最后将其后面的数据向前移动一位。
由此可以看到,不管插入还是删除,其移动或者复制的数据都是很小的一部分,因此性能很可观。
其实现代码如下:
package com.kfyty.core.lang.util;
import com.kfyty.core.support.Pair;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.AbstractList;
import java.util.Arrays;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.RandomAccess;
/**
* 描述: 链式数组,适用于大量随机插入随机删除
*
* @author kfyty725
* @date 2022/11/19 9:52
* @email kfyty725@hotmail.com
*/
public class LinkedArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Serializable {
/**
* 每个数组最小容量
*/
private static final int MIN_CAPACITY = 16;
/**
* 每个数组的容量
* 该参数对性能影响非常大,建议预计总元素数 / 200
*/
private final int capacity;
/**
* 集合元素数量
*/
private transient int size;
/**
* 头节点
*/
private transient LinkedArrayNode head;
/**
* 尾节点
*/
private transient LinkedArrayNode tail;
public LinkedArrayList() {
this(MIN_CAPACITY);
}
public LinkedArrayList(int capacity) {
super();
this.size = 0;
this.capacity = capacity;
this.head = this.tail = new LinkedArrayNode(capacity);
}
public LinkedArrayList(Collection<E> collection) {
this(collection.size() / 200, collection);
}
public LinkedArrayList(int capacity, Collection<E> collection) {
this(capacity);
this.addAll(collection);
}
@Override
public int size() {
return this.size;
}
@Override
public Iterator<E> iterator() {
return new Iter();
}
@Override
public boolean add(E e) {
this.add(size(), e);
return true;
}
@Override
public E get(int index) {
if (index < (this.size >> 2)) {
return Optional.ofNullable(this.findNode(index)).map(v -> v.getValue().get(v.getKey())).orElse(null);
}
return Optional.ofNullable(this.findNodeReverse(index)).map(v -> v.getValue().get(v.getKey())).orElse(null);
}
@Override
public E set(int index, E element) {
if (index < (this.size >> 2)) {
return Optional.ofNullable(this.findNode(index)).map(v -> v.getValue().set(v.getKey(), element)).orElse(null);
}
return Optional.ofNullable(this.findNodeReverse(index)).map(v -> v.getValue().set(v.getKey(), element)).orElse(null);
}
@Override
public void add(int index, E element) {
if (index < (this.size >> 2)) {
Optional.ofNullable(this.findNode(index)).ifPresent(v -> v.getValue().add(v.getKey(), element));
} else {
Optional.ofNullable(this.findNodeReverse(index)).ifPresent(v -> v.getValue().add(v.getKey(), element));
}
this.size++;
}
@Override
public E remove(int index) {
E remove = index < (this.size >> 2)
? Optional.ofNullable(this.findNode(index)).map(v -> v.getValue().remove(v.getKey())).orElse(null)
: Optional.ofNullable(this.findNodeReverse(index)).map(v -> v.getValue().remove(v.getKey())).orElse(null);
this.size--;
return remove;
}
@Override
public int indexOf(Object o) {
int index = -1;
for (E e : this) {
index++;
if (o == null && e == null || o != null && o.equals(e)) {
return index;
}
}
return -1;
}
@Override
@SuppressWarnings("unchecked")
public int lastIndexOf(Object o) {
int index = this.size();
LinkedArrayNode node = this.tail;
while (node != null) {
for (int i = node.nodeSize - 1; i > -1; i--) {
index--;
E e = (E) node.element[i];
if (o == null && e == null || o != null && o.equals(e)) {
return index;
}
}
node = node.prior;
}
return -1;
}
@Override
public void clear() {
LinkedArrayNode node = this.head;
while (node != null) {
node.nodeSize = 0;
Arrays.fill(node.element, null);
LinkedArrayNode next = node.next;
node.next = node.prior = null;
node = next;
}
this.size = 0;
this.tail = this.head;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof List)) {
return false;
}
Iterator<?> self = this.iterator();
Iterator<?> other = ((List<?>) o).iterator();
while (self.hasNext() && other.hasNext()) {
if (!Objects.equals(self.next(), other.next())) {
return false;
}
}
return !(self.hasNext() || other.hasNext());
}
private Pair<Integer, LinkedArrayNode> findNode(int index) {
LinkedArrayNode node = this.head;
while (node != null && index >= node.nodeSize) {
index -= node.nodeSize;
node = node.next;
}
return node == null ? null : new Pair<>(index, node);
}
private Pair<Integer, LinkedArrayNode> findNodeReverse(int index) {
index = this.size - 1 - index;
LinkedArrayNode node = this.tail;
while (node != null && index >= node.nodeSize) {
index -= node.nodeSize;
node = node.prior;
}
return node == null ? null : new Pair<>(node.nodeSize - 1 - index, node);
}
@SuppressWarnings("unchecked")
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
this.size = 0;
this.head = this.tail = new LinkedArrayNode(this.capacity);
for (int i = 0, count = s.readInt(); i < count; i++) {
this.add((E) s.readObject());
}
}
private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeInt(this.size);
LinkedArrayNode node = this.head;
while (node != null) {
for (int i = 0; i < node.nodeSize; i++) {
s.writeObject(node.element[i]);
}
node = node.next;
}
}
/**
* 迭代器
*/
private class Iter implements Iterator<E> {
/**
* 当前游标
*/
int cursor;
/**
* 当前的 size
*/
int curSize;
/**
* 当前的节点游标
*/
int curNodeCursor;
/**
* 当前的节点
*/
LinkedArrayNode curNode;
Iter() {
this.curSize = LinkedArrayList.this.size;
this.curNode = LinkedArrayList.this.head;
}
@Override
public boolean hasNext() {
return cursor < LinkedArrayList.this.size;
}
@Override
@SuppressWarnings("unchecked")
public E next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
if (curSize != LinkedArrayList.this.size) {
throw new ConcurrentModificationException();
}
// 由于 remove() 操作,可能有的节点容量为 0,因此需要找到下一个容量大于 0 的节点
while (curNode != null && curNodeCursor >= curNode.nodeSize) {
curNodeCursor = 0;
curNode = curNode.next;
}
cursor++;
return curNode == null ? null : (E) curNode.element[curNodeCursor++];
}
@Override
public void remove() {
this.cursor--;
this.curNode.remove(--curNodeCursor);
this.curSize--;
LinkedArrayList.this.size--;
}
}
/**
* 链式数组节点
*/
private class LinkedArrayNode {
/**
* 元素
*/
Object[] element;
/**
* 节点大小
*/
int nodeSize;
/**
* 前驱节点
*/
LinkedArrayNode prior;
/**
* 后继节点
*/
LinkedArrayNode next;
LinkedArrayNode(int capacity) {
this.nodeSize = 0;
this.element = new Object[capacity];
}
LinkedArrayNode(LinkedArrayNode prior) {
this(capacity);
this.prior = prior;
}
@SuppressWarnings("unchecked")
protected E get(int index) {
this.checkRange(index, size() - 1);
return (E) this.element[index];
}
@SuppressWarnings("unchecked")
protected E set(int index, E e) {
this.checkRange(index, size() - 1);
E result = (E) element[index];
element[index] = e;
return result;
}
@SuppressWarnings("unchecked")
protected E remove(int index) {
this.checkRange(index, size() - 1);
E result = (E) element[index];
if (index < nodeSize - 1) {
System.arraycopy(element, index + 1, element, index, nodeSize - 1 - index);
}
element[--nodeSize] = null;
if (this.nodeSize == 0) {
this.removeNode(this);
} else {
this.shiftNextIfNecessary();
}
return result;
}
/**
* 添加元素到指定索引
* 1、索引范围检查
* 2、如果插入索引位置大于等于节点大小,则尝试添加到下一个节点
* 3、如果节点大小大于等于最大容量,则创建新的节点
* 4、如果索引位置已有数据,则将该索引及其后面的元素后移
* 5、将元素插入该索引位置
*
* @param index 索引
* @param e 元素
*/
protected void add(int index, E e) {
this.checkRange(index);
if (index >= nodeSize) {
this.addToNextIfNecessary(index, e);
return;
}
if (this.nodeSize >= capacity) {
this.newNextIfNecessary();
}
if (this.nodeSize > 0 && index < this.nodeSize && index < capacity - 1) {
int tempSize = this.nodeSize == capacity ? this.nodeSize - 1 : this.nodeSize;
System.arraycopy(element, index, element, index + 1, tempSize - index);
}
this.element[index] = e;
if (this.nodeSize < capacity) {
this.nodeSize++;
}
}
/**
* 索引范围检查
*
* @param index 索引
*/
private void checkRange(int index) {
this.checkRange(index, size());
}
/**
* 索引范围检查
*
* @param index 索引
* @param limit 最大限制
*/
private void checkRange(int index, int limit) {
if (index < 0 || index > limit) {
throw new ArrayIndexOutOfBoundsException("Size: " + size() + ", index: " + index);
}
}
/**
* 移除节点
*
* @param node 节点
*/
private void removeNode(LinkedArrayNode node) {
if (node.prior == null) {
if (node.next == null) {
return;
}
node.next.prior = null;
LinkedArrayList.this.head = node.next;
return;
}
if (node.next == null) {
LinkedArrayList.this.tail = node.prior;
node.prior.next = null;
return;
}
node.prior.next = node.next;
node.next.prior = node.prior;
}
/**
* 当前节点大小大于等于最大容量,尝试创建新的节点
* 1、如果 next 为空,则直接创建新的数组节点
* 2、如果 next 不为空并且容量未满,则将 next 的全部元素后移一位
* 3、如果 next 不为空并且容量已满,则创建新的数组节点插入其后
* 4、将当前元素的最后一个元素移动到 next 的第一位元素
*/
private void newNextIfNecessary() {
if (this.next == null) {
LinkedArrayList.this.tail = this.next = new LinkedArrayNode(this);
} else if (next.nodeSize < capacity) {
System.arraycopy(next.element, 0, next.element, 1, next.nodeSize);
} else {
LinkedArrayNode temp = new LinkedArrayNode(this);
temp.next = this.next;
this.next.prior = temp;
this.next = temp;
}
this.next.element[0] = this.element[nodeSize - 1];
this.next.nodeSize++;
}
/**
* 如果插入索引位置大于等于节点大小,则尝试添加到下一个节点
* 1、如果当前节点未满并且索引在最大容量范围内,则尝试左移元素,并直接返回
* 2、否则如果 next 为空,则创建新的数组节点
* 3、调用 next.add() 插入元素
*
* @param index 索引
* @param e 元素
*/
private void addToNextIfNecessary(int index, E e) {
if (this.nodeSize < capacity && index < capacity) {
this.shiftLeft(index, e);
return;
}
if (this.next == null) {
LinkedArrayList.this.tail = this.next = new LinkedArrayNode(this);
}
this.next.add(index - nodeSize, e);
}
/**
* 尝试左移元素
* 1、如果 next 为空,则无需左移,直接添加即可
* 2、计算需要左移的元素数量,如果 next 节点大小小于左移的元素数量,则无需左移,直接调用 next.add() 添加即可
* 3、需要左移,执行左移,并将该元素插入当前节点最后的位置
*
* @param index 索引
* @param e 元素
*/
private void shiftLeft(int index, E e) {
if (this.next == null) {
this.element[nodeSize++] = e;
return;
}
int move = index - nodeSize;
if (next.nodeSize < move) {
this.next.add(index - nodeSize, e);
return;
}
this.doShiftLeft(move);
this.element[nodeSize++] = e;
}
/**
* 执行左移,即将 next 的前 move 个元素移动到当前节点
* 1、如果移动后的 next 节点大小小于等于 move,则说明 next 节点已空,直接移除 next 节点
* 2、否则将 next 节点剩余的元素移动到数组头部
*
* @param move 需要移动的元素数量
*/
private void doShiftLeft(int move) {
System.arraycopy(next.element, 0, this.element, this.nodeSize, move);
this.nodeSize += move;
if (next.nodeSize <= move) {
Arrays.fill(next.element, 0, next.nodeSize, null);
this.next = this.next.next;
this.next.prior = this;
return;
}
System.arraycopy(next.element, move, next.element, 0, next.nodeSize - move);
Arrays.fill(next.element, next.nodeSize - move, next.nodeSize, null);
next.nodeSize -= move;
}
/**
* 删除时,尝试移动下一个节点的元素到当前节点,避免碎片化过多
* 仅当当前节点的空余元素大于等于下一个节点的元素时,执行移动,并删除下一个节点,避免移动元素过多
*/
private void shiftNextIfNecessary() {
if (this.next == null || LinkedArrayList.this.capacity - this.nodeSize < this.next.nodeSize) {
return;
}
System.arraycopy(next.element, 0, this.element, this.nodeSize, next.nodeSize);
Arrays.fill(next.element, 0, next.nodeSize, null);
this.nodeSize += this.next.nodeSize;
this.removeNode(this.next);
}
}
}
下面放上测试代码:
@Test
public void test5() {
int count = 1000000;
Random random = new Random();
List<Integer> list = new LinkedArrayList<>(5000);
// List<Integer> list = new ArrayList<>(5000);
// List<Integer> list = new LinkedList<>();
long start = System.currentTimeMillis();
list.add(1);
for (int i = 0; i < count; i++) {
list.add(random.nextInt(list.size()), random.nextInt(count));
}
for (int i = 0; i < count - 10; i++) {
list.remove(random.nextInt(list.size()));
}
System.out.println(list);
System.out.println(System.currentTimeMillis() - start + " ms");
}
测试的时候,需要传入一个数组初始化容量,这个参数对 LinkedArrayList 的性能影响非常大,一千万级别的顺序插入,不同的参数的性能差距在两个数量级以上!
最后,在上述测试代码下,一百万的数据随机插入以及随机删除: LinkedArrayList 的性能在 2.2 秒左右; ArrayList 的性能在 153 秒左右; LinkedList 的性能在 (还在等待中。。。算了吧) 秒左右。