1、LinkedList底层实现原理
LinkedList底层的数据结构是基于双向循环链表的,且头结点中不存放数据,如下:
从图中可以看出,有头节点和数据节点,头节点是结构必须的,数据节点即是存放数据的节点。
每一个节点都有三个元素,一个是当前节点的数据,也就是data部分,data左边就是指向该节点的上一个节点的指针,右边就是指向该节点的下一个节点的指针。
既然是双向链表,那么必定存在一种数据结构——我们可以称之为节点,节点实例保存业务数据,前一个节点的位置信息和后一个节点位置信息,如下图所示:
(1)、数组与链表的结构比较
数组是将元素在内存中连续存放,由于每个元素占用内存相同,可以通过下标迅速访问数组中任何元素。但是如果要在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。同样的道理,如果想删除一个元素,同样需要移动大量元素去填掉被移动的元素。如果应用需要快速访问数据,很少插入和删除元素,就应该用数组。
链表中的元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起,每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针。
如果要访问链表中一个元素,需要从第一个元素开始,一直找到需要的元素位置。但是增加和删除一个元素对于链表数据结构就非常简单了,只要修改元素中的指针就可以了。如果应用需要经常插入和删除元素你就需要用链表。
a)、内存存储区别
数组从栈中分配空间, 对于程序员方便快速,但自由度小。
链表从堆中分配空间, 自由度大但申请管理比较麻烦.
b)、逻辑结构区别
数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费。
链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。(数组中插入、删除数据项时,需要移动其它数据项)
c)总的来说
1、存取方式上,数组可以顺序存取或者随机存取,而链表只能顺序存取;
2、存储位置上,数组逻辑上相邻的元素在物理存储位置上也相邻,而链表不一定;
3、存储空间上,链表由于带有指针域,存储密度不如数组大;
4、按序号查找时,数组可以随机访问,时间复杂度为O(1),而链表不支持随机访问,平均需要O(n);
5、按值查找时,若数组无序,数组和链表时间复杂度均为O(1),但是当数组有序时,可以采用折半查找将时间复杂度降为O(logn);
6、插入和删除时,数组平均需要移动n/2个元素,而链表只需修改指针即可;
7、空间分配方面:
数组在静态存储分配情形下,存储元素数量受限制,动态存储分配情形下,虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且如果内存中没有更大块连续存储空间将导致分配失败;
链表存储的节点空间只在需要的时候申请分配,只要内存中有空间就可以分配,操作比较灵活高效;
2、自定义LinkedList
ExtList接口跟 上一篇的一样 Java基础(二)集合之ArrayList(集合第二篇)
ExtLinkedList实现类,完全根据源码的业务逻辑实现,并添加注释+自己的理解
public class ExtLinkedList<E> implements ExtList<E>{
/**
* 链表的大小
*/
transient int size = 0;
/**
* 指向第一个节点的指针。
*/
transient Node first;
/**
* 指向最后一个节点的指针。
*/
transient Node<E> last;
@Override
public void add(E e) {
//默认在列表的尾部添加元素
linkLast(e);
}
//在指定下标位置添加元素
@Override
public void add(int index, E e) {
//检查索引下标是否越界
checkPositionIndex(index);
if (index == size)
//如果索引的位置是在链表的最后,直接调用在尾部添加元素
linkLast(e);
else
//否则,在下标之后插入元素
linkBefore(e, node(index));
}
/**
* 在下标位置插入元素
* @param e 元素
* @param succ 要插入索引位置的节点
*/
void linkBefore(E e, Node<E> succ) {
//获取原索引位置的节点的上一个节点
final Node<E> pred = succ.prev;
//1、新建一个节点,该节点的左节点指向原索引位置节点的上一个节点,
// 右节点指向原索引位置节点,元素为传入的元素
final Node<E> newNode = new Node<>(pred, e, succ);
//2、将原索引位置节点的左节点指定为新添加的节点
succ.prev = newNode;
if (pred == null)
//3、如果索引位置节点的上一个节点为空,则是空链表,将新添加的节点设置为第一个节点
first = newNode;
else
//4、如果索引位置节点的上一个节点不为空,则将其右节点设置为新添加的节点
pred.next = newNode;
//5、链表数量加1
size++;
}
//在列表末尾添加元素
private void linkLast(E e) {
//获取最后一个节点
final Node<E> l = last;
//1、新建一个节点,将该节点的左节点指向原来的最后一个节点,
// 右节点指向null(最后一个节点都是指向null),元素为传入元素
final Node<E> newNode = new Node<>(l, e, null);
//2、将当前的链表的最后一个节点设置为新添加的节点
last = newNode;
if (l == null)
//3、如果最后一个节点为空,则是空链表,将新添加的节点设置为第一个节点
first = newNode;
else
//4、如果最后一个节点不为空,将原来最后一个节点的右节点指针指向新添加的节点
l.next = newNode;
//5、链表数量加1
size++;
}
@Override
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
//根据传入对象删除节点
@Override
public boolean remove(Object o) {
//如果出入对象为空,循环删除链表中的空值
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
@Override
public int getSize() {
return size;
}
@Override
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
/**
* 自定义Node节点
* @param <E>
*/
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;
}
}
/**
* 返回指定元素索引处的(非null)节点。
*/
Node<E> node(int index) {
// assert isElementIndex(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;
}
}
/**
* 取消链表非空节点x。
*/
E unlink(Node<E> x) {
final E element = x.item;//当前节点元素
final Node<E> prev = x.prev;//左节点
final Node<E> next = x.next;//右节点
if (prev == null) {
//1、如果上一个节点为空,则是删除第一个节点,将将链表的头部设置原来第二个节点即可
first = next;
} else {
//2、如果上一个节点不为空,则将要要删除节点的上一个节点的右节点指向要删除节点的下一个节点
prev.next = next;
//删除当前节点的左指针(将当前节点的左节点设置为空)
x.prev = null;
}
if (next == null) {
//3、如果下一个节点为空,则要删除的节点是最后一个节点,将将链表的尾部设置原来倒数第二个节点即可
last = prev;
} else {
//4、如果下一个节点不为空,则要将要删除节点的下一个节点的左节点指向要删除节点的上一个节点
next.prev = prev;
//删除当前节点的右指针(将当前节点的右节点设置为空)
x.next = null;
}
//5、删除当前节点元素
x.item = null;
//6、链表数量减一
size--;
//7、返回删除的元素
return element;
}
//检查索引下标是否越界
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//检查元素索引是否越界
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 判断参数是否是现有元素的索引。
*/
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
/**
* 判断参数是否是迭代器或添加操作的有效位置的索引。
*/
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
/**
* 抛出数组下标越界异常信息
*/
private String outOfBoundsMsg(int index) {
return "索引位置: "+index+", 数组大小: "+size;
}
}
测试代码:
//测试ArrayList的删除元素速度
@Test
public void testExtArrayList1(){
ExtList<User> list = new ExtArrayList<>();
for (int i = 0; i < 10000000; i++) {
list.add(new User((long)i,"张三"+i));
}
System.out.println("删除元素开始时间:"+System.currentTimeMillis());
list.remove(0);
System.out.println("删除元素结束时间:"+System.currentTimeMillis());
}
//测试LinkedList的删除元素速度
@Test
public void testExtLInkedList(){
ExtList<User> list = new ExtLinkedList<>();
for (int i = 0; i < 10000000; i++) {
list.add(new User((long)i,"张三"+i));
}
System.out.println("删除元素开始时间:"+System.currentTimeMillis());
list.remove(0);
System.out.println("删除元素结束时间:"+System.currentTimeMillis());
}
可以得出结果:
LinkedList的删除速度比ArrayList快一些。
学习Java基础知识,做一个简单的记录。
上一篇:Java基础(二)集合之ArrayList(集合第二篇)
下一篇:暂无