List之LinkedList集合源码分析
这里方便阅读代码,加深印象。自定义LsList接口,自定义LsLinkedList实现自定义接口。LsLinkedList类模仿LinkedList类实现了LsList的核心接口:size、add、get、remove,并分析LinkedList源码。
定义接口
public interface LsList<E> {
/**
* 定义数组长度
*/
int size();
/**
* 添加元素
* @param e
* @return
*/
boolean add(E e);
/**
* 使用下标查询元素
* @param index
* @return
*/
E get(int index);
/**
* 删除元素
* @return
*/
E remove(int index);
}
实现接口
public class LsLinkedList<E> implements LsList<E> {
transient int size = 0;
/**
* 第一个节点
*/
transient LsLinkedList.Node<E> first;
/**
* 最后一个节点
*/
transient LsLinkedList.Node<E> last;
@Override
public int size() {
return size;
}
@Override
public boolean add(E e) {
linkLast(e);
return true;
}
@Override
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
@Override
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
E unlink(LsLinkedList.Node<E> x) {
// 获取到当前删除节点的详细信息
final E element = x.item;
// 获取到当前删除节点的下一个节点信息
final LsLinkedList.Node<E> next = x.next;
// 获取当前删除节点的上一个节点信息
final LsLinkedList.Node<E> prev = x.prev;
// 如果当前删除节点的上一个节点为null,则当前删除的节点为第一个节点
// 所以需要将第一个节点修改成当前删除节点的下一个节点
// 此时该链表的第一个节点为当前删除节点的下一个节点
if (prev == null) {
first = next;
} else {
// 否则,将当前删除节点的上一个节点的下一个节点 修改成当前删除节点的下一个节点
// 并且将当前删除节点连接的上一个节点置为null
prev.next = next;
x.prev = null;
}
// 如果当前删除节点的下一个节点为null,则说明当前删除的节点为最后一个节点
// 将最后一个节点置为当前删除节点的上一个节点
// 此时该链表的最后一个节点为当前删除节点的上一个节点
if (next == null) {
last = prev;
} else {
// 否则,当前删除节点的下一个节点连接的上一个节点置为当前删除节点的上一个节点
// 当前删除节点连接的下一个节点信息置为null
next.prev = prev;
x.next = null;
}
// 将当前删除节点的信息设置为null,交给GC垃圾回收掉
x.item = null;
// 大小-1
size--;
return element;
}
/**
* 添加节点
*
* @param e
*/
void linkLast(E e) {
// 封装当前自定义元素
// 获取当前节点的最后一个节点
final LsLinkedList.Node<E> l = last;
final LsLinkedList.Node<E> newNode = new Node<>(l, e, null);
// 当前节点为最后一个节点
last = newNode;
if (l == null)
// 如果链表没有最后一个节点,说明当前新增的元素是第一个
first = newNode;
else
// 原来的最后一个节点的下一个节点就是当前新增的节点
l.next = newNode;
size++;
}
// 链表的节点
private static class Node<E> {
/**
* 节点元素值
*/
E item;
/**
* 当前节点的下一个节点
*/
LsLinkedList.Node<E> next;
/**
* 当前节点的上一个节点
*/
LsLinkedList.Node<E> prev;
/**
* 使用构造函数传递参数
*
* @param prev
* @param element
* @param next
*/
Node(LsLinkedList.Node<E> prev, E element, LsLinkedList.Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
Node(E element) {
this.item = element;
}
public void setPrev(Node<E> prev) {
this.prev = prev;
}
public void setNext(Node<E> next) {
this.next = next;
}
}
LsLinkedList.Node<E> node(int index) {
// 如果查询的下标小于折半的值,则从第一个节点往后查找节点
if (index < (size >> 1)) {
LsLinkedList.Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
// 如果查询的下标大于折半的值,则从最后一个节点往前查询节点
LsLinkedList.Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
/**
* 检查数组是否越界
*/
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException("下标位置越界了!index为:"+index);
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
}
LinkedList集合操作图解
举例:若此时有一个装有四个元素(五月天、突然好想你、顽固、好好)分别对应下标为(0、1、2、3)的链表集合,并对该集合作新增、移除元素操作。分析几种情况:新增元素(在头、尾新增元素;在中间新增元素)、移除元素(移除头、尾元素;移除中间位置元素)。
LinkedList图
注:下图中pre代表该节点指向的上一个节点,next代表该节点指向的下一个节点。
注意头节点(五月天)的pre指向的上一个节点为null,尾节点(好好)next指向下一个节点为null。
移除头节点(五月天)
注:阅读LinkedList集合移除元素方法源码可知,若需要移除某节点元素,只需要修改删除元素前后节点的pre、next指向即可。
若需要移除头节点(五月天),则需要将该节点的下一个节点(突然好想你)设置成第一个节点,并修改它的pre指向null,然后将删除的节点置为null,交给GC回收。
移除中间节点(突然好想你)
若需要移除中间节点(突然好想你),则需要将该移除节点的下一个节点(顽固)的pre指向移除节点的上一个节点(五月天),并将移除节点的上一个节点(五月天)的next指向移除节点的下一个节点(顽固),然后将删除的节点置为null,交给GC回收。
新增节点(知足)
若需要新增节点(知足),则需要将头节点(五月天)的next指向该节点,将节点(突然好想你)的pre指向该节点即可。
测试
public class TestLsLinkedList {
public static void main(String[] args) {
LsLinkedList<String> lsLinkedList = new LsLinkedList<>();
// 添加元素
lsLinkedList.add("五月天");
lsLinkedList.add("突然好想你");
lsLinkedList.add("顽固");
lsLinkedList.add("好好");
// 移除元素
lsLinkedList.remove(1);
// 遍历集合
for (int i = 0; i < lsLinkedList.size; i++) {
System.out.println(lsLinkedList.get(i));
}
// 打印集合大小
System.out.println(lsLinkedList.size());
System.out.println(lsLinkedList.get(3));
}
}
五月天
顽固
好好
3
Exception in thread “main” java.lang.IndexOutOfBoundsException: 下标位置越界了!index为:3
总结
- LinkedList集合查询、修改效率低。因为集合采用链表实现,所以定位需要查询、修改的数据需要遍历整个集合,虽然集合采用二分查找极大的提高了定位元素的效率,但是效率还是相对较低的。
- LinkedList集合新增、删除效率高。因为如果在集合中插入或删除数据,只需要将节点的pre、next指向更改即可。如果需要在指定位置新增、删除数据则效率相对较低,因为集合需要定位到新增、删除元素的位置的效率相对较低。
如文章有误,欢迎批评指正,欢迎交流。
参考:蚂蚁课堂集合源码分析视频。