目录
ArrayList引发的思考
优点:查询快
缺点:
1、增删慢,消耗CPU性能(情况一:向指定索引上添加或删除元素,需要往前或往后移动索引后的元素 情况二:如果原数组中空间不够了,需要扩容,创建一个新的数组然后将原数组中的元素复制到新数组上)
2、比较浪费内存空间(申请的空间必须是连续的,并且并不是申请几个空间就用几个空间)
那么有没有一个集合能够弥补ArrayList的缺陷呢?这里我们就要提到双向链表了。
说起数据结构中的链表,我们不得不提这种数据结构的优缺点:
优点:添加和删除元素比较快,因为只是移动指针,并且不需要判断是否需要扩容
缺点:查询和遍历效率比较低,因为链表不能想顺序表那样随机存取,在第i个位置上执行存或者取的操作,顺序表仅需访问一次,而链表需要从头开始依次访问i次
而LinkedList就是基于双向循环链表实现的首先来看一个LinkedList的继承实现树:
LinkedList不仅是双向循环链表的实现,除了可以当做单链表、双链表、循环链表来操作外,它实现了Deque接口,它还可以当做栈、队列和双端队列来使用。
LinkedList底层原理
首先我们来重点说说双向链表这个数据结构
每一个节点都由3个部分组成,一个是数据本身item,一个是指向下一个节点的next指针,还有就是指向上一个节点的prev指针,另外,双向链表还有一个 first 指针,指向头节点,和 last 指针,指向尾节点。在LinkedList类中通过私有的静态内部类Node作为每一个数据的封装。具体实现如下:
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;
}
}
再看该类的成员变量
成员变量size用来记录双向链表的大小,first节点用来指向链表的头,last用来指向链表的尾
再看类的构造方法
public LinkedList() {
}
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
该类有两个构造方法,一个是空参数构造方法,另外一个是通过已有的集合进行构造。我们再来看看通过集合构造里面调用的addAll()方法做了什么
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;
}
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
可见,调用集合调用方法,如果传入的集合不为空,就将集合中的元素遍历为每个值封装成一个Node节点,然后依次链接到链表上。
注意如果传入的集合为空,那么会报出空指针异常
接下来看看LinkedList的基本操作:添加,删除,遍历,查询等
先看添加,从双向链表的结构来看,添加元素可以在链表的头、尾、以及中间的任意位置添加新的元素。因为 LinkedList 有头指针和尾指针,所以在表头或表尾进 行插入元素只需要 O(1) 的时间,而在指定位置插入元素则需要先遍历一下链表, 所以复杂度为 O(n)。首先看看在头部添加元素:
从头部添加元素,只要把头指针first指向新的node节点,新的node节点的next指针指向原先first指针指向的node,再把原先first指针指向的node的prev指针指向新的node节点就可以了
private void linkFirst(E e) {
final Node<E> f = first; //使用临时node
final Node<E> newNode = new Node<>(null, e, f);
//封装新的node,并把新node的nex指向f
first = newNode;
if (f == null) //判断first是否为空
last = newNode;
else
f.prev = newNode; //把f的prev指向新的node
size++; //链表长度加1
modCount++; //记录链表被修改的次数
}
在尾部添加,其实和在头部添加一样,只是把first换成了last,逻辑一样
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++;
}
那么在中间位置添加元素呢?
void linkBefore(E e, Node<E> succ) { //表示在在succ节点前面添加e元素
// assert succ != null;
final Node<E> pred = succ.prev; //获取succ的前面节点
final Node<E> newNode = new Node<>(pred, e, succ); //把e封装成节点,并把prev指向succ前面节点,把next指向succ节点
succ.prev = newNode; //然后把succ的prev指向新的节点
if (pred == null)
first = newNode;
else
pred.next = newNode; //把succ的前节点的next只想新的节点
size++; //链表长度+1
modCount++; //修改次数+1
}
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++;
}
在指定元素之前、之后添加元素,需要改动被插入以及元素前后或者后面的节点的指针
从头部、尾部、中间位置删除、修改元素都与添加类似。如果学过数据结构,那么这些操作自然很熟悉,甚至我前面说的都显得是多余的。
LinkedList强大在哪里
前面我们说过LinkedList实现了链表、栈、队列等诸多数据结构,那么对这些数据结构的操作都对应了LInkedList类中的那些方法呢?来看下面的一个表格
public boolean add(E e) | 在链表尾部插入元素 |
public void add(int index, E element) | 在链表指定索引位置插入元素 |
public void addFirst(E e) | 在链表头部插入元素 |
public void addLast(E e) | 在链表尾部插入元素 |
public boolean addAll(Collection<? extends E> c) | 在尾部追加新集合元素 |
public boolean addAll(int index, Collection<? extends E> c) | 在指定索引位置追加集合中的元素 |
public void push(E e) | 对于栈:将元素入栈 对于链表:头部插入与元素 |
public E pop() | 对于栈:将栈顶元素弹出返回 对于链表:返回并删除链表头元素 |
public E pollFirst() | 检索返回并删除链表第一个元素,如果链表为空返回null |
public E pollLast() | 检索返回并删除链表最后一个元素,如果链表为空返回null |
public E peek() | 检索返回但不删除链表第一个元素,如果链表为空则返回null |
public E peekFirst() | 检索返回但不删除链表第一个元素,如果链表为空则返回null |
public E peekLast() | 检索返回但不删除链表最后一个元素,如果链表为空则返回null |
public E element() | 检索但不删除此列表的头 |
public E set(int index, E element) | 将指定索引位置的元素修改 |
public boolean offer(E e) | 在链表尾部添加元素 |
public boolean offerFirst(E e) | 在链表头部添加元素 |
public boolean offerLast(E e) | 在链表尾部添加元素 |
public E get(int index) | 返回链表中指定索引的元素 |
public E getFirst() | 返回链表第一个元素 |
public E getLast() | 返回链表中最后一个元素 |
public E remove() | 检索返回并删除链表第一个元素,如果链表为空抛出 NoSuchElementException |
public E remove(int index) | 删除指定索引位置的元素,并将该元素返回 |
public boolean remove(Object o) | 删除在该链表中第一次出现该元素位置的元素 |
public E removeFirst() | 删除并返回链表中的第一个元素 Throws: |
public E removeLast() | 删除并返回链表中最后一个元素 Throws: |
public boolean removeFirstOccurrence(Object o) | 删除链表中第一次出现的元素O |
public boolean removeLastOccurrence(Object o) | 删除链表中最后一次出现的元素O |
手写LinkedList加深印象
下面我们自己尝试手写一个简单的LinkedList。由于底层是链表,如果学过数据结构,我相信即使不看源码自己也能手写一个能够储存若干数据的链表,并完成增删改查等功能。按照ArrayList分析思路,最好的思路是看一下LinkedList底层源码,并自己手写一个简单的LinkedList,这里源码不在贴出,手写的LinkedList仅仅实现了小部分功能,仅供参考
//带头尾指针的双向链表
public class MyLinkedList<E> {
public MyLinkedList() {
}
;
//定义数据结点
private class Node<E> {
Node previous;//指向前一个结点的指针
Node next;//指向后一个结点的指针
E element;//储存数据的变量空间
public Node(Node previous, Node next, E element) {
super();
this.element = element;
this.next = next;
this.previous = previous;
}
public Node(E element) {
super();
this.element = element;
}
}
private Node first;//链表的头指针
private Node last;//链表的尾指针
private int size;
public void add(Object o) {
Node node = new Node(o);
//如果第一个节点为空。即第一次增加节点
if (first == null) {
node.previous = null;
node.next = null;
first = node;
last = node;
} else {//如果不是第一次增加节点
node.previous = last;//将原来链表的尾指针指向的结点设置为新增结点的前驱
node.next = null;//新结点的后继为空
last.next = node;//将原来链表的尾指针指向的结点的后继设置为新增结点
last = node;//更新链表尾指针指向新增结点
}
size++;
}
public Object get(int index) {
//检查索引是否合法
checkIndex(index);
//获取指定索引位置的结点对象
Node node=getNodeByIndex(index);
return node!=null?node.element:null;
}
public int size() {
return size;
}
public void remove(int index) {
checkIndex(index);
Node node=getNodeByIndex(index);
if(node!=null){
Node prior=node.previous;
Node next=node.next;
if (prior!=null){
//将要删除的结点的前驱结点的后继指针指向要删结点的后继结点
prior.next=next;
}else {
//如果要删除的结点前驱为空,证明要删除的头节点,此时移动头指针
first=first.next;
}
if (next!=null){
//将要删除的结点的后继结点的前驱指针指向要删结点的前驱结点
next.previous=prior;
}else {
//如果要删除的结点后继为空,证明要删除的尾节点,此时移动尾指针
last=last.previous;
}
}
size--;
}
public void checkIndex(int index) {
if (index < 0 || index > size - 1) {
throw new RuntimeException("非法的索引:" + index);
}
}
public Node getNodeByIndex(int index) {
if(size==0){
return null;
}
Node temp = first;
for (int i = 0; i < index; i++) {
temp = temp.next;
}
return temp;
}
}