上文讲解了ArrayList的底层实现原理,感兴趣的小伙伴可以去看下,本文重点讨论LinkedList集合。
首先说下ArrayList和LinkedList的区别:(相同点都是有序的~)
① ArrayList底层数据结构是动态数组,LinkedList底层数据结构是双向链表。
② 查询或者修改的时候,ArrayList比LinkedList的效率更高,因为LinkedList是线性的基于链表的数据存储方式,所以需要移动指针从前往后依次查找,即使源码中(下面会详细说明)用了二分法,但是效率还是不如ArrayList底层基于数组,直接通过索引定位,效率极快。
③ 增加或者删除的时候,LinkedList比ArrayList的效率更高,因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动,而LinkedList只需要修改prev和next的指针引用即可。
综上所述,Arraylist适用于查询或者修改比较多的场景,LinkeList适用于查询/修改较少,增加和删除较多的场景。
纯手写LinkedList源码(白话文分析):
package com.example;
public class MyLinkedList<E> implements MyList<E> {
//E LinkedList 存放的数据类型
/**
* 集合的大小
*/
transient int size = 0;
/**
* 第一个节点
*/
transient Node<E> first;
/**
* 最后一个节点
*/
transient 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) {
// 检查我们index是否越界
checkElementIndex(index);
// 通过二分法查找具体Node对象/节点的item
return node(index).item;
}
/** 【删除原理:改变相互引用的指针】
* 步骤1:当前要删除Node节点的上一个的next指向当前要删除节点的下一节点
* 步骤2:当前要删除Node节点的下一个节点的prev指向当前要删除节点的上一个节点
* 步骤3:当前要删除Node节点/对象,所有属性置为null,等待gc回收
* @param index
* @return
*/
@Override
public E remove(int index) {
// 检查我们index是否越界
checkElementIndex(index);
// node(index)首先获取到当前删除节点,然后再删除unlink(),即调整指针位置
return unlink(node(index));
}
/**
* 添加我们的节点,作为最后一个元素
* @param e
*/
void linkLast(E e) {
// 获取当前的最后一个节点
final Node<E> l = last;
// 封装我们当前自定义元素
final Node newNode = new Node<E>(l, e, null);
// 当前新增节点肯定是链表中最后一个节点(当前新增节点赋给last)
last = newNode;
if (l == null) //注意,走到这一步last变了,但l没变
// 如果我们链表中没有最后一个节点说明当前新增的元素是第一个
first = newNode;
else
// 原来的最后一个节点的下一个节点就是当前新增的节点
l.next = newNode;
size++;
}
/**
* 链表:其实可以理解为全表扫描(折半查找)
* 数组:直接通过索引定位,效率极快
* @param index index小于折半值,从头开始查;index大于折半值,从尾开始查
* @return
*/
Node<E> node(int index) {
/* 举个例子
现在链表中有1-100节点,如果想查第88个节点,正常情况下从1查到88,即索引从0到87
写JDK的人比较聪明,运用了折半查找(也成为二分法),查询步骤如下:
size / 2 = 50,如果88大于50,那么查50~100就行了(索引49-99)
*/
// size >> 1 → size/2 → if里面的判断解析为 index < size/2
// 假设链表中有1-10节点,查询下标为0,又因为0<10/2,所以在0-4之间找
if (index < (size >> 1)) {
// 获取到第一个节点
Node<E> x = first;
for (int i = 0; i < index; i++) {
// 如果index小于折半值,从头(0)查询到index【基于索引】
x = x.next;
}
return x;
} else {
// 获取到最后一个节点
Node<E> x = last;
for (int i = size - 1; i > index; i--) {
// 如果index大于折半值,从尾(size-1)查询到index【基于索引】
x = x.prev;
}
return x;
}
// 1-10 | 3 1-3 | 7 10-7
}
/**
* 删除节点,重新连接链表
* @param x 当前删除的Node节点
* @return
*/
E unlink(MyLinkedList.Node<E> x) {
// 获取到当前删除的节点的元素值
E element = x.item;
// 获取当前删除元素的下一个节点
Node<E> next = x.next;
// 获取当前删除元素的上一个节点
Node<E> prev = x.prev;
if (prev == null) { /** 判断prev */
// 如果prev为空,说明当前删除节点是第一个节点,需要把next置为第一个节点(first为全局变量)
first = next;
} else {
// 如果prev不为空, 上一个Node节点的next指向下一个Node节点
prev.next = next;
// 当前删除节点的prev变为空,告诉给gc实现回收
x.prev = null;
}
if (next == null) { /** 判断next */
// 如果next为空,说明当前删除节点是最后一个节点,需要把prev置为最后一个节点(last为全局变量)
last = prev;
} else {
// 如果next不为空,下一个Node节点的prev指向上一个Node节点
next.prev = prev;
// 当前删除节点的next变为空 告诉给gc实现回收
x.next = null;
}
// 当前删除的节点的元素值置为空
x.item = null;
size--;
return element;
}
/**
* 链表中的节点
* @param <E>
*/
private static class Node<E> {
// 节点元素值 zhangsan,lisi..
E item;
// 当前节点的下一个node(节点/对象)
MyLinkedList.Node<E> next;
// 当前节点的上一个node
MyLinkedList.Node<E> prev;
// 使用构造函数传递参数
Node(MyLinkedList.Node<E> prev, E element, MyLinkedList.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;
}
}
private void checkElementIndex(int index) {
if (!isElementIndex(index)) {
throw new IndexOutOfBoundsException("index已经越界啦~~~");
}
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
public static void main(String[] args) {
Node node1 = new Node("第一关游戏");
Node node2 = new Node("第二关游戏");
node1.next = node2;
node2.prev = node1;
System.out.println("node:" + node1);
}
}
在上述main方法最后一行打个断点,Debug启动,会发现node1和node2是相互引用的,验证了LinkedList底层是基于双向链表。
此时,测试一下我们手写的LinkesList:
package com.example.test;
import com.example.MyLinkedList;
public class Test001 {
public static void main(String[] args) {
MyLinkedList<String> linkedList = new MyLinkedList<>();
linkedList.add("001");
linkedList.add("002");
linkedList.add("003");
linkedList.remove(1);
System.out.println(linkedList.get(1));
}
}
会发现,当我们删除索引为1的元素,那么003则会替代002的位置,该测试把我们的增,删,查都用上了,改的话无非就是先调用node(即查询指定索引对应的Node节点),然后替换Node的item属性为新元素即可。
【总结】
链表数据底层原理实现:双向链表头尾相接
① 在底层中使用静态内部类Node节点存放节点元素
三个属性 prev(关联的上一个节点),item(当前的值) ,next(下一个节点)
② add原理是如何实现? 答案: 一直在链表之后新增
③ get原理:采用折半查找 范围查询定位node节点
④ remove原理:改变相互引用的指针