前言:我们学习java时都知道ArrayList实现List接口,LinkedList也实现List接口,但我们平时用的时候LinkedList却很少被用到。那么,LinkedList什么时候该用到呢?内部又是如何实现的呢?本文对此进行详细说明,希望能够助君更上一层楼。
我们在使用ArrayList(动态数组)时有个明显的缺点,就是当容量接近满值的时候,会进行扩容,JDK默认扩容为1.5倍,那么,当数据量极大时,就会造成内存空间的大量浪费!
能否用到多少就申请多少内存?
没错LinkedList(链表)可以办到这一点。
相信计算机专业的同学一定在数据结构课上学到过链表这种数据结构,链表是一种链式存储的线性表,所有元素的内存地址不一定是连续的,内存结构如下:
上图可以看出,我们的LinkedList每一个节点都有一个地址空间,用来存储我们下一个节点的地址,虽然地址并不连续,但数据的存储并不会受到任何影响。
在真正的设计中,为了查询、添加、删除的方便,我们的链表还需要有头尾指针以及前驱指针
那么,让我们归结一下,链表首先需要一个头指针,指向头结点。一个尾节点,存储尾节点。然后每个节点都需要一个存储它的内部元素的属性,一个指针指向他的前驱,一个节点指向他的后继。
接着,我们先写一下节点的类 👇
private static class Node<E> {
E element;
Node<E> prev;
Node<E> next;
public Node(Node<E> prev, E element, Node<E> next) {
this.prev = prev;
this.element = element;
this.next = next;
}
我们的节点是LinkedList的属性,所以将节点放入我们的LinkedList中,如下👇
public class LinkedList<E> extends AbstractList<E> {
private Node<E> first;
private Node<E> last;
private static class Node<E> {
E element;
Node<E> prev;
Node<E> next;
public Node(Node<E> prev, E element, Node<E> next) {
this.prev = prev;
this.element = element;
this.next = next;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
if (prev != null) {
sb.append(prev.element);
} else {
sb.append("null");
}
sb.append("_").append(element).append("_");
if (next != null) {
sb.append(next.element);
} else {
sb.append("null");
}
return sb.toString();
}
}
@Override
public void clear() {
size = 0;
first = null;
last = null;
}
@Override
public E get(int index) {
return node(index).element;
}
@Override
public E set(int index, E element) {
Node<E> node = node(index);
E old = node.element;
node.element = element;
return old;
}
@Override
public void add(int index, E element) {
rangeCheckForAdd(index);
// size == 0
// index == 0
if (index == size) { // 往最后面添加元素
Node<E> oldLast = last;
last = new Node<>(oldLast, element, null);
if (oldLast == null) { // 这是链表添加的第一个元素
first = last;
} else {
oldLast.next = last;
}
} else {
Node<E> next = node(index);
Node<E> prev = next.prev;
Node<E> node = new Node<>(prev, element, next);
next.prev = node;
if (prev == null) { // index == 0
first = node;
} else {
prev.next = node;
}
}
size++;
}
@Override
public E remove(int index) {
rangeCheck(index);
Node<E> node = node(index);
Node<E> prev = node.prev;
Node<E> next = node.next;
if (prev == null) { // index == 0
first = next;
} else {
prev.next = next;
}
if (next == null) { // index == size - 1
last = prev;
} else {
next.prev = prev;
}
size--;
return node.element;
}
@Override
public int indexOf(E element) {
if (element == null) {
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (node.element == null) return i;
node = node.next;
}
} else {
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (element.equals(node.element)) return i;
node = node.next;
}
}
return ELEMENT_NOT_FOUND;
}
/**
* 获取index位置对应的节点对象
* @param index
* @return
*/
private Node<E> node(int index) {
rangeCheck(index);
if (index < (size >> 1)) {
Node<E> node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
} else {
Node<E> node = last;
for (int i = size - 1; i > index; i--) {
node = node.prev;
}
return node;
}
}
@Override
public String toString() {
StringBuilder string = new StringBuilder();
string.append("size=").append(size).append(", [");
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (i != 0) {
string.append(", ");
}
string.append(node);
node = node.next;
}
string.append("]");
return string.toString();
}
相比较我们的ArrayList,LinkedList是否有什么特别的优点吗?
我们的每种数据结构设计出来都有其特别的使用之处。总结一下:
数组(ArrayList)的随机访问速度非常快,但是指定位置添加、删除操作时需要将后续元素全部顺移。
LinkedList则恰恰相反,我们在使用链表做指定位置的添加删除操作时,所需的操作非常少,但随机访问速度却非常慢。
基于这些属性,我们在使用中该如何选择呢?
如果频繁在尾部进行添加、删除操作,动态数组、双向链表均可选择
如果频繁在头部进行添加、删除操作,建议选择使用双向链表
如果有频繁的(在任意位置)添加、删除操作,建议选择使用双向链表
如果有频繁的查询操作(随机访问操作),建议选择使用动态数组
所以,我们在应聘岗位的时候,会有一则能够在不同情境使用不同的数据结构
选择最合适的数据结构进行使用是非常重要的!
以上!!