Java数据结构——ArrayList和LinkedList

时间:2023-02-04 10:16:09

ArrayList 和 LinkedList都是基于List接口实现,本质是顺序表,提供增删改查数据的基本功能,但效率上各有优劣。接下来通过源码分别看下它们管理和操作数据的具体原理。

List接口

List接口定义了列表最基本的操作,如获取,查找,删除,更新,插入等。

public interface List<E> extends Collection<E> {
int size();//获取列表长度

void add(int index, E element); //插入

E get(int index); //访问

int indexOf(Object o); //查找

E remove(int index); //删除

E set(int index, E element); //更新

default void sort(Comparator<? super E> c) { //默认排序方法
Collections.sort(this, c);
}

ArrayList

ArrayList是一个顺序表,内部使用数组保存数据,当容量不足时会自动扩容。
特点:访问某个元素的效率较高,但是查找元素或者在list中间插入元素时效率一般。

特性:
1. 允许重复元素
2. 元素是无序的
3. 操作都是基于普通数组

基本操作

插入,删除,修改,查找,获取操作如下:
1. 插入操作需要先检查是否需要扩容数组,如果数据是插入到尾部的则可直接赋值,否则插入到中部则需arraycopy移动数据腾出空间,再插入数据。
2. 删除操作直接通过arraycopy操作覆盖目标值的位置
3. 修改元素值只需要在数组中直接替换。
4. 查找操作是简单的遍历查找,因此效率可能不高
5. 获取元素操作通过index直接返回,比较高效。

插入数据

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

transient Object[] elementData;

//中间插入元素。
//1.先检查是否需要扩容
//2.移动数组元素腾出位置,然后插入元素
public void add(int index, E element) {
...
ensureCapacityInternal(size + 1); //判断是否需要扩容
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
}

访问和查找数据

    //获取元素——直接返回数组项
public E get(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

return (E) elementData[index];
}

//查找——遍历
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}

删除数据

    //删除,通过arraycopy操作,覆盖要删除的元素
public E remove(int index) {
...
modCount++;
E oldValue = (E) elementData[index];

int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work

return oldValue;
}

修改数据

    //修改元素值
public E set(int index, E element) {
...
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}

数组扩容

往ArrayList插入新元素时会判断当前数组长度是否足够,需要时将数据copy到更长的一个数组中,实现扩容。

    private static final int DEFAULT_CAPACITY = 10; //每次默认扩展10
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;//最大容量

private void ensureCapacityInternal(int minCapacity) {
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
modCount++;
//计算要扩展的最小长度
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0) //
newCapacity = hugeCapacity(minCapacity);
//数组扩展到新长度newCapacity
elementData = Arrays.copyOf(elementData, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}

LinkedList

LinkedList是一个双向链表,每个节点包含了上一个和下一个节点的信息,LinkedList只记录头尾2个节点作为成员变量。

对比ArrayList:
1. 优势在于删除和插入等操作,直接操作节点无需移动数据;
2. get和set操作的效率会比ArrayList低,因为ArrayList可以直接访问数组元素,而LinkedList多了寻找节点的过程。
3. 查找操作上LinkedList与ArrayList效率相当,因为它们的数据都是无序的,查找时都是简单的遍历。

public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{

//记录数据长度
transient int size = 0;
//头节点
transient Node<E> first;
//尾节点
transient Node<E> last;

//节点类定义
private static class Node<E> {
E item;//当前节点数据
Node<E> next;
Node<E> prev;

Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
}

基本操作

插入数据

  1. 找到目标位置的节点
  2. 将新节点与前后节点关联
    Java数据结构——ArrayList和LinkedList
    public void add(int index, E element) {
if (index == size)
linkLast(element); //插入到尾部
else
linkBefore(element, node(index)); //插入到中部
}

void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}

访问或修改数据

通过指定index获取或修改该节点的数据。

    //获取节点值
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}

//修改节点值
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}

//查找节点
Node<E> node(int index) {
if (index < (size >> 1)) {//位置在链表上半段则从链头开始找
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {//位置在链表后半段,从链尾开始找
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}

查找元素位置

给出具体数据,查找该数据在链表中的位置。

    public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}

删除节点

输入数据实例或者在链表中的下标值,删除该节点。

    public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));//寻找节点后删除其与前后节点的关联
}

//删除节点
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;

if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}

if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}

x.item = null;
size--;
modCount++;
return element;
}

LinkedList的队列特性

LinkedList实现了Deque接口,因此也可以当做FIFO或LIFO的队列使用。
用作FIFO队列的方法:

    public E poll() {//队头出列
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}

public E peek() {//获取队头
final Node<E> f = first;
return (f == null) ? null : f.item;
}

public boolean offer(E e) {//新数据排入队尾
return add(e);
}

用作LIFO队列的方法:

    public E pop() {
return removeFirst();
}

public void push(E e) {
addFirst(e);
}

综合对比

  • ArrayList的操作数据都是通过普通的数组实现,随机访问和修改元素比较快,但插入/删除等操作需要移动较多数据,比较适合在访问数据比较频繁的场景使用
  • LinkedList是双向链表,在随机访问和修改都需要先去查找节点,但是在插入/删除数据时不需要移动数据,比较适合用在插入/删除频繁的场景中。
  • LinkedList还实现了队列接口,通过头或尾进行顺序访问的同时可以删除节点,充分发挥了链表的优势。

工具类Arrays

Arrays.java提供了很多静态方法用于操作数组,例如:复制 , 填充, 查找, 排序, 数组转换成ArrayList等等。

public class Arrays {

//数组转换成ArrayList
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}

//复制数组
public static <T,U> T[] copyOfRange(U[] original, int from, int to, Class<? extends T[]> newType) {
int newLength = to - from;
if (newLength < 0)
throw new IllegalArgumentException(from + " > " + to);
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, from, copy, 0,
Math.min(original.length - from, newLength));
return copy;
}

//填充数组
public static void fill(Object[] a, Object val) {
for (int i = 0, len = a.length; i < len; i++)
a[i] = val;
}

//二分查找
public static int binarySearch(Object[] a, int fromIndex, int toIndex,
Object key) {
rangeCheck(a.length, fromIndex, toIndex);
return binarySearch0(a, fromIndex, toIndex, key);
}

//排序
public static void sort(Object[] a, int fromIndex, int toIndex) {
rangeCheck(a.length, fromIndex, toIndex);
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, fromIndex, toIndex);
else
ComparableTimSort.sort(a, fromIndex, toIndex, null, 0, 0);
}

}