2019独角兽企业重金招聘Python工程师标准>>>
如果说ArrayList是基于数组实现的List,那么LinkedList是基于链表实现的List。
1.定义
public class LinkedList
extends AbstractSequentialList
implements List, Deque, Cloneable, java.io.Serializable
可以看得到LinkedList继承了AbstractSequentialList。实现了List,Deque. 后面两个和ArrayList一样,说明可以被克隆和序列化。
而AbstractSequentialList基础自AbstractList,而且还重新实现了get,set,add,remove,等等方法。
AbstractSequentialList的代码如下:
package java.util;
public abstract class AbstractSequentialList extends AbstractList {
protected AbstractSequentialList() {
}
public E get(int index) {
try {
return listIterator(index).next();
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
public E set(int index, E element) {
try {
ListIterator e = listIterator(index);
E oldVal = e.next();
e.set(element);
return oldVal;
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
public void add(int index, E element) {
try {
listIterator(index).add(element);
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
public E remove(int index) {
try {
ListIterator e = listIterator(index);
E outCast = e.next();
e.remove();
return outCast;
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
public boolean addAll(int index, Collection extends E> c) {
try {
boolean modified = false;
ListIterator e1 = listIterator(index);
Iterator extends E> e2 = c.iterator();
while (e2.hasNext()) {
e1.add(e2.next());
modified = true;
}
return modified;
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
public Iterator iterator() {
return listIterator();
}
public abstract ListIterator listIterator(int index);
}
而Dqueue接口 是一个双向队列,也就是既可以先入先出,又可以先入后出,再直白一点就是既可以在头部添加元素又在尾部添加元素,既可以在头部获取元素又可以在尾部获取元素。看下Deque的定义:
public interface Deque extends Queue
2.底层存储
private transient Entry header = new Entry(null, null, null);
private transient int size = 0;
size和ArrayList里面的size一样,记录容器元素的个数。那这个Entry类型的变量header是个什么鬼。
Entry是个内部类,来描述链表的节点的信息,代码如下:
//描述链表节点的类
private static class Entry {
E element; //存储的对象
Entry next;//链表的下一个节点元素
Entry previous;//链表的上一个节点元素
Entry(E element, Entry next, Entry previous) {
this.element = element;
this.next = next;
this.previous = previous;
}
}
可以看得出ArrayList底层是采用双向链表来实现的。
数据结构双向链表是包含两个指针的,pre指向前一个节点,next指向后一个节点,但是第一个节点head的pre指向null,最后一个节点的tail指向null。
3.构造函数
//默认构造
public LinkedList() {
链表的头和尾都指向了自己
header.next = header.previous = header;
}
//将一个集合的元素来构造自己,这些元素按其 collection 的迭代器返回的顺序排列
public LinkedList(Collection extends E> c) {
this();
addAll(c);
}
从默认构造函数可以看得出这是一个双向循环链表,如果是双向不循环链表的话,应该是:
header.next=header.previous=null。
4.增加
有8个增加,5个增加是实现Dqueue里面的添加函数,其余3个是实现List接口里面的添加函数,然后实现Dqueue基本是调用实现List里面的添加函数,所以我们使用的时候直接调用List里面的添加函数即可。可以少压一次栈。
//这5个add是LinkedList实现Dqueue里面的添加函数,其实都是调用实现List接口里面的函数
//将元素加到第一个,可以看得到,是加到header的后面,因为header就是一个空头,里面没有存储元素
public void addFirst(E e) {
addBefore(e, header.next);
}
//将元素加列表的尾部
public void addLast(E e) {
addBefore(e, header);
}
//封装链表插入操作,分两步走
//1.其实就是在构造的时候,自己的netx和previous指好
//2.自己的前节点的next指向自己,自己后结点的pre指向自己。
private Entry addBefore(E e, Entry entry) {
Entry newEntry = new Entry(e, entry, entry.previous);
//自己的前节点的next指向自己
newEntry.previous.next = newEntry;
//自己后结点的pre指向自己
newEntry.next.previous = newEntry;
//记数加1
size++;
modCount++;
return newEntry;
}
public boolean offer(E e) {
return add(e);
}
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
public boolean offerLast(E e) {
addLast(e);
return true;
}
//下面三个增加是实现List接口里面的添加函数
//将元素加列表的尾部
public boolean add(E e) {
addBefore(e, header);
return true;
}
//添加一个集合元素到list中
public boolean addAll(Collection extends E> c) {
//其实还是调用在指定位置添加一个集合到list中
return addAll(size, c);
}
//在指定位置添加一个集合元素到list中
public boolean addAll(int index, Collection extends E> c) {
//检查下标是否越界
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);
//将集合转换为数组
Object[] a = c.toArray();
//要增加元素的长度
int numNew = a.length;
if (numNew==0)
return false;
modCount++;
//找出要插入元素的前后节点
//找出其后节点,如果位置和size大小相等(为什么不是size-1,因为header占了一位),则他的下一个节点是header
//否则要查找index位置的节点,这个就是他的后一个节点
Entry successor = (index==size ? header : entry(index));
//他的前一个节点,就是index位置的前一个节点。
Entry predecessor = successor.previous;
for (int i=0; i e = new Entry((E)a[i], successor, predecessor);
//将它前一个节点的next指向自己
predecessor.next = e;
//后面的元素插入到这个元素的后面
predecessor = e;
}
//将index位置的节点的前指针指向自己这样就完成了链表的操作。
successor.previous = predecessor;
size += numNew;
return true;
}
双向链表的增加比起数组的增加稍微是要麻烦的理解一点,自己画图应该不难理解,总结起来,就是一句话主要就是插入会改变前节点的next和后一节点的pre,主要把前一节点的next指向自己,后一节点的pre指向自己,即可。
5.删除
//删除第一个容器元素
public E removeFirst() {
return remove(header.next);
}
//删除最后一个容器元素
public E removeLast() {
return remove(header.previous);
}
//删除指定的容器元素
public boolean remove(Object o) {
if (o==null) {
for (Entry e = header.next; e != header; e = e.next) {
if (e.element==null) {
remove(e);
return true;
}
}
} else {
//循环遍历节点,然后找到节点,然后删除。时间复杂度O(n)
for (Entry e = header.next; e != header; e = e.next) {
if (o.equals(e.element)) {
remove(e);
return true;
}
}
}
return false;
}
//删除指定位置的容器元素
public E remove(int index) {
//entry(index)是找到这个节点
return remove(entry(index));
}
//删除指定的节点,供自己调用
private E remove(Entry e) {
if (e == header)
throw new NoSuchElementException();
E result = e.element;
//将自己的前一节点的后指针执行自己的后一节点
e.previous.next = e.next;
//讲自己后一节点的前指针指向自己的前节点
//讲自己置为null,给gc回收
e.next.previous = e.previous;
e.next = e.previous = null;
e.element = null;
//大小减一
size--;
modCount++;
return result;
}
删除则要简单一点,删除了自己之后,主要
将自己的前一节点的后指针执行自己后一节点
讲自己后一节点的前指针指向自己的前节点
而且可以看到remove(Object o)时间负责度为O(n),而remove(int)的时间复杂度度为O(n/2),因为里面用到了二分查找的办法。所以删除的时候要注意了,要选用正确的方法删除。(其实我转载了一篇博客专门介绍这个LinkenList的局限。)所以以前所说的增删快的删有时也是很慢的。
6.修改容器元素的值
public E set(int index, E element) {
Entry e = entry(index);
E oldVal = e.element;
e.element = element;
return oldVal;
}
这个比较简单,查找到,然后修改即可
7.查找
我们知道链表和数组相比,查找比数组要慢的非常多,数组直接定位,而链表每次我们只能拿到一个头部,所以不管找什么,我们都要从头开始遍历起,而LinkedList使用了双向循环链表,这样遍历起来就会快很多,既可以从头往后找,又可以从后往前找。直到找到index位置。
//查找指定index的链表里面元素
public E get(int index) {
return entry(index).element;
}
//这个方法很重要,基本上查找都是使用这个方法来进行查找。
//这儿就显示双向循环链表的好处,既可以从头往后遍历,又可以从后往前遍历
//这儿使用了二分查找的方法,效率要高很多
private Entry entry(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+size);
Entry e = header;
//如果下标小于size的一般,就从头往后遍历,找到元素
if (index < (size >> 1)) {
for (int i = 0; i <= index; i++)
e = e.next;
} else {否则从后往前遍历
for (int i = size; i > index; i--)
e = e.previous;
}
return e;
}
到这里我们明白,基于双向循环链表实现的LinkedList,通过索引Index的操作时低效的,index所对应的元素越靠近中间所费时间越长。而向链表两端插入和删除元素则是非常高效的(如果不是两端的话,都需要对链表进行遍历查找)。
8.其余的LinkedList的操作
8.1 查找元素是否在容器中
public boolean contains(Object o) {
return indexOf(o) != -1;
}
public int indexOf(Object o) {
int index = 0;
if (o==null) {
for (Entry e = header.next; e != header; e = e.next) {
if (e.element==null)
return index;
index++;
}
} else {
for (Entry e = header.next; e != header; e = e.next) {
if (o.equals(e.element))
return index;
index++;
}
}
return -1;
}
public int lastIndexOf(Object o) {
int index = size;
if (o==null) {
for (Entry e = header.previous; e != header; e = e.previous) {
index--;
if (e.element==null)
return index;
}
} else {
for (Entry e = header.previous; e != header; e = e.previous) {
index--;
if (o.equals(e.element))
return index;
}
}
return -1;
}
要遍历,低效。
打完收工。