LinkedList 也是一个比较常见的数据结构,链表。在C/C++ 中,链表也是一个典型的线性结构,链表分为单向跟双向的两种链表。在java里面的LinkedList是一个双向的链表。java
链表最好的好处就在于来一个数据加一个长度,没有多余的冗余, 也是支持存储各类对象的(最好使用泛型)。node
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
跟ArrayList实现的同一个接口,所以就不贴了。数组
deque 即双端队列。是一种具备队列和栈的性质的数据结构。双端队列中的元素能够从两端弹出,其限定插入和删除操做在表的两端进行。数据结构
这个后续学队列再去详细的介绍。函数
跟ArrayList同样,都是支持克隆和序列化的。this
transient int size = 0;
用来记录 链表的大小,一样是transient的。spa
transient Node<E> first;
表示头结点。code
transient Node<E> last;
表示最后一个节点。对象
由于链表也是一个典型的线性结构,跟数组类似,可是也有不一样,由于链表在逻辑地址(咱们脑海中)是一个连续的地址,而在物理地址(电脑内存中)中却不是连续的一块,而是一个点,一个点的,这个节点持有上个节点,和下个节点的引用,如图:继承
由于逻辑地址并不要求是一块内存,所以链表在这方面仍是要优于数组的。
可是正是不须要内存连续,所以要额外的须要持有上一个节点和下一个节点的引用。单个内存的开销变大。
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; } }
这是LinkedList中的内部类,Node ,每add一个数据就会有一个Node对象,每个节点都有先后两个节点的引用,这就造成了一个双向链表。
跟ArrayList 相同的,Node 节点也是 transient 的修饰的,所以,后面也会重写private void writeObject(java.io.ObjectOutputStream s) 和 private void readObject(java.io.ObjectInputStream s) 这两个方法。目的跟ArrayList应该是同样的。
/** * Constructs an empty list. */ public LinkedList() { }
不解释。
public LinkedList(Collection<? extends E> c) { this(); //添加 addAll(c); }
这个方法能够直接将已经存在的Collection 转换为一个LinkList 包括ArrayList 还有其余 子类。
----------------------
在这里发现,LinkedList的构造方法是没有指定长度的构造方法,这是跟数组的一个比较 重要的区别(这不是废话吗。。。。。。)
---------------------
public boolean add(E e) { //调用另一个方法 linkLast(e); return true; }
因为add方法内部是调用的另一个方法,所以贴出另外的方法。
/** * Links e as last element. */ void linkLast(E e) { //last 是记录的最后一个元素 final Node<E> l = last; //new 一个Node 的节点 final Node<E> newNode = new Node<>(l, e, null); //last更新为新生成的 last = newNode; //若是last为null证实这是第一次添加,那么他就是第一个,也是最后一个。 if (l == null) first = newNode; else l.next = newNode; //大小+1 size++; //fail-fast 机制 modCount++; }
有这个方法名字就能够很直白的看出来,是添到了链表的尾部。
其中的 new Node<>(l,e,null) 能够去看上面的 贴出来的Node的构造方法,
Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; }
以前的last设为 新的 prev,next 设置为null item 设置为存储的数据。
public void add(int index, E element) { //下标检测,是否超出 checkPositionIndex(index); //若是位置正好等于 size ,就直接插入到最后 if (index == size) linkLast(element); else //插入到指定位置 linkBefore(element, node(index)); }
因为链表也是一个线性结构,所以也支持够插入到指定位置。
void linkBefore(E e, Node<E> succ) { // assert succ != null; //插入以前的index 位置上的元素的 前一个元素 final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); //将原 index 位置上的元素的前一个 设置为新的节点 succ.prev = newNode; if (pred == null) //若是此节点的前一个是null,意味着插入的是首个位置 first = newNode; else //前 index 位置的元素的前一个的 下一个引用,设置为新节点 pred.next = newNode; size++; modCount++; }
画一个图应该是更好理解。
------------------------------------------------
灵魂画手:
大致的分为三步, 若是插入的位置是 0 ,跟这个差很少。
------------------------------------------------------
public void addFirst(E e)
public void addFirst(E e) { linkFirst(e); }
private void linkFirst(E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; }
这个从名字就很好看出来,就是添加到位置 0。
不过为何不用add(0,e),而是再写一个方法呢?
----------------------------------------------------------
public void addLast(E e)
public void addLast(E e) { linkLast(e); }
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++; }
这两个方法,大同小异吧,具体实现也是差很少的,不过就是明明有了add具体位置插入,为何不用这个方法,而是另外再写呢?
public boolean addAll(Collection<? extends E> c)
public boolean addAll(Collection<? extends E> c) { return addAll(size, c); }
public boolean addAll(int index, Collection<? extends E> 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; }
这两个 方法是将 Collection 的某个集合里面的所有数据放入到 LinkedList 中,感受用的不是不少,问的也不会不少,我也没注意过,不看了。
--------------------------------------------------------
public E remove(int index)
public E remove(int index) { //边界检测 checkElementIndex(index); //执行删除的函数 return unlink(node(index)); }
Node<E> node(int index) { // assert isElementIndex(index); //若是index 是小于 size/2的 就从前找 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; } }
上面你这个函数就是 node(index) 的方法,能够看出他是返回了 index位置上的 node的引用,由于链表在内存中是不连续的,因此只能经过遍历链表去查找 对应的数据,
这个判断 index<(seze>>1) 的思想是 二分法的思想,将整个链表分为2份,看从哪边近,就从哪边开始,这也是双链表的优势,若是是单链表,就只能从前日后遍历了。
E unlink(Node<E> x) { // assert x != null; //首先记录三个节点,当前的,前一个,后一个,每一个对结构有调整的(add,remove ) // 通常第一步 都是记录这三个 final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; //若是前一个是null,就意味着删除的是first节点 if (prev == null) { //直接将 first指向 第二个 first = next; } else { //若是不是first元素,将要删除的前一个元素的 next 指向 当前元素的下一个 prev.next = next; //将当前的前一个元素置空。 x.prev = null; } //意味删除的是最后一个节点 if (next == null) { //last 前移一个 last = prev; } else { // 不然将下一个节点 的 前一个引用 设为 当前节点的前一个 next.prev = prev; //将 当前节点的 下一个置空 x.next = null; } //数据置空 x.item = null; size--; modCount++; return element; }
---------------------------------
来个图:
忽然感受本身颇有画图天赋。。。。23333333
-------------------------------------------
public boolean remove(Object o)
public boolean remove(Object o) { if (o == null) { //若是 须要删除的对象是null for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { //对象不是null 就是用 equals方法 for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; }
这个其实只是分了 null和非null 两种状况,null 使用== ,非null 使用 equals 方法进行比较。
其中调用的删除的方法 仍是 unlink(x) 。
时刻记得,链表虽然是一个线性表,可是没法像 数组那样,直接下标偏移量 找到删除的元素,这个必须须要进行一个 遍历,将整个链表 遍历完成。
一样的,这里删除一样要避免删除的时候,你想删除的若是是int 类型的数据,切记要转换为 包装类,不然调用的方法是remve(int)。
---------------------------------------------
其余的remove方法
public E removeFirst()
public E removeLast()
----------------------------------------------
public E get(int index) { checkElementIndex(index); return node(index).item; }
整个get方法不多,判断是否越界,而后返回 index 位置节点的 item 。
--------------------
其余 get方法
public E getFirst()
public E getLast()
------------------------
其余相似 get 方法
public E peek()
/** * Retrieves, but does not remove, the head (first element) of this list. * * @return the head of this list, or {@code null} if this list is empty * @since 1.5 */ public E peek() { final Node<E> f = first; return (f == null) ? null : f.item; }
public E element()
/** * Retrieves, but does not remove, the head (first element) of this list. * * @return the head of this list * @throws NoSuchElementException if this list is empty * @since 1.5 */ public E element() { return getFirst(); }
public E getFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return f.item; }
public E poll()
/** * Retrieves and removes the head (first element) of this list. * * @return the head of this list, or {@code null} if this list is empty * @since 1.5 */ public E poll() { final Node<E> f = first; return (f == null) ? null : unlinkFirst(f); }
这三个方法都是返回first ,不过第一、3方法是 若是 first 是 null 就返回null ,第2个方法内部调用的是getFirst(),这个方法:若是是空,就 抛出异常。
一、2两个方法只是返回,不删除元素,3方法是返回切删除了元素。
这里面有不少相似的方法,就不贴了,贴了浪费流量。
public boolean contains(Object o)
public boolean contains(Object o) { return indexOf(o) != -1; }
这个是是否包含 某个元素。须要传入的是对象。
private void writeObject(java.io.ObjectOutputStream s)
private void readObject(java.io.ObjectInputStream s)
具体代码不贴了,感受这个LinkedList写的有点啰嗦了。
public Object[] toArray()
public <T> T[] toArray(T[] a)
-----------------------------------------------------------------------------
不保证代码彻底正确,也不保证代码是最优。
仅仅是根据本身的理解,写出来直观的代码,方便理解。
错误请指出,感激涕零!