LinkedList和ArrayList不同,是双向链表,每个元素持有对上一个和下一个元素的引用,便于插入(add(index))和删除操作,查找操作(get(index))需要遍历每个元素,故适用于频繁添加和删除元素,查找元素较少的场合。
LinkedList中的数据结构用例和HashMap中一样,存放的是模拟实体Entry对象。
定义头部指针
private transient Entry<E> header = new Entry<E>(null, null, null);
头部指针header相当于LinkedList中的初始坐标,header的next指向第一个Entry元素。有了header,才能访问第一个Entry对象。
实体Entry类
private static class Entry<E> {
E element;
Entry<E> next;
Entry<E> previous;
Entry(E element, Entry<E> next, Entry<E> previous) {
this.element = element;
this.next = next;
this.previous = previous;
}
}
Entry类有next和previous指针,指向后一个和前一个Entry元素,通过这两个指针,LinkedList才得以实现双向链表。
LinkedList构造函数
public LinkedList() {
header.next = header.previous = header;
}
初始化LinkedList时,是个空列表,里面没有Entry元素存在,header的next和previous指针都指向了header自身。
查询链表第一个元素
public E getFirst() {
if (size==0)
throw new NoSuchElementException();
return header.next.element;
}
链表的第一个元素是头部指针header的next指向的元素。
查询链表最后一个元素
public E getLast() {
if (size==0)
throw new NoSuchElementException();
return header.previous.element;
}
链表的最后一个元素是头部指针header的previous指向的元素,由此可见双向链表是一个循环结构,最后一个元素的next指针指向了头部指针header。
向链表添加元素
public boolean add(E e) {
addBefore(e, header);
return true;
}
添加元素时,是将新的元素添加到链表的尾部。
private Entry<E> addBefore(E e, Entry<E> entry) {
Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
newEntry.previous.next = newEntry;
newEntry.next.previous = newEntry;
size++;
modCount++;
return newEntry;
}
分为两种场景:
添加第一个元素Entry时:
方法体第一行代码:Entry的element指向自身,next指向了头部指针header,previous指针指向了header.previous,因为header的previous和next都指向header,所以,Entry的previous指向了header。
第二行和第三行代码:header的next和previous都指向了Entry元素。
就是first元素与header的next与previous指针相互指向。
添加第二个元素到N个Entry元素时:
方法体第一行代码:Entry的element还是指向自身,next还是指向了头部指针header,previous指向了header.previous元素,就是链表中添加的第一个元素。这样第二个元素与第一个元素的关系就建立起来了。
第二行代码:第一个元素的next指针指向Entry。
第三行代码:头部指针header的previous指针指向Entry。
可以看出,每个新增加的Entry元素,header都会成为这个Entry的next指向,header的previous也会指向这个新添加的元素,这个新添加的元素就在链尾,所以,header的previous指向的是链尾的元素。
这样就行HashMap中的链表添加元素相反,HashMap中的链表添加元素,是从链头开始添加,链头的元素后移,LinkedList添加元素,是添加到链尾。
查找链表中指定位置的元素
public E get(int index) {
return entry(index).element;
}
private Entry<E> entry(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+size);
Entry<E> e = header;
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;
}
查找时,先把索引值index与链表的size值的一半进行比较,如果大于size/2,就从last处开始向前遍历,向前遍历size/2-index次就可找到index处的元素。
如果index小于size/2,就从first处开始向后遍历,向后遍历index次就可找到index处的元素。
采用先让index与size/2比较的算法,缩小了查找的范围,发挥了双向链表的优势,提高了查找index的效率(相对于单向链表)。
查找的前提,都是先把头部指针header赋予一个临时变量开始的,所以头部指针应该是LinkedList数据结构的主脉。
移除链表中指定位置的元素
public E remove(int index) {
return remove(entry(index));
}
remove()采用了分而治之的思想,先调用entry(index)找到要移除的元素,再调用remove(E)方法把Entry元素移除。
private E remove(Entry<E> e) {
if (e == header)
throw new NoSuchElementException();
E result = e.element;
e.previous.next = e.next;
e.next.previous = e.previous;
e.next = e.previous = null;
e.element = null;
size--;
modCount++;
return result;
}
移除元素时,Entry把preivious元素的next指针指向了Entry本身的next指向的元素,把next元素的previous指针指向了本身指向的previous元素。然后把next、previous、element都设置为null。
这样Entry前后两个元素就建立起了关联关系,双向链表又维护了完整。
从中可以看出,这个Entry被移除掉,性能的开销只是在查找index位置的Entry元素上,所以移除的效率很高(相对于移动数组元素而言)。
查询元素在链表中的位置
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;
}
这个方法是设置一个int型的index计数变量,然后从前向后遍历链表中的元素,每遍历一次index的值+1,当遍历的元素与传入的对象值相同时,返回index值。
总结:
双向链表的内部结构是引用指针双向指向的结构,这种结构的特点是插入数据快,删除数据也快,查找数据慢,因为要遍历所有元素。所以适用于查找少,而增加和删除、修改数据多的场景。